From sandbox to production
This is an end-to-end runnable path an agent (or the human wiring one up) can copy
line by line: create an account, prove the loop with the mail simulator, verify a
real recipient, harden the mailbox for production, and make a first production send
with an honest exit code. Every command is the real rly CLI against the
deployed API. Where a contract is owned by another page, this walkthrough links to
it rather than restating it.
The arc:
- Sign up and verify your email and mobile number.
- Send to the simulator — the one address that skips the sandbox recipient gate.
- Add a real recipient — the sandbox's recipient rule.
- Move to production — what changes, observed as contracts (not internals).
- Set a safety posture — allowlist mode, human review, a delivery webhook.
- First production send with
--strictso a non-delivery is a non-zero exit.
New accounts start in sandbox — a time-boxed trial mode with a low daily send cap and a recipient-confirmation gate. See the limits reference for the exact sandbox caps and tier ceilings; this page owns none of those numbers.
1. Sign up and verify
CLI sign-up bootstraps a separate account and needs a dashboard-issued signup
code (rls_cli_…). Create your very first account in the dashboard at
https://app.replylayer.ai/signup; mint a CLI signup code there for any further
CLI-provisioned account. Sign-up requires a mobile number with country code,
accepting the legal agreements, and acknowledging the URL-reputation disclosure.
rly signup \
--email [email protected] \
--phone +13125550123 \
--accept-terms \
--accept-web-risk \
--cli-signup-code rls_cli_xxxxxxxxOn success the CLI stores the returned API key in your local credential file and, if verification is pending, prints a next step:
rly auth verify --code 123456 # the 6-digit code emailed at signup
rly auth verify-phone --code 593104 # the 6-digit code sent by SMSThe SMS code is texted after you verify the email (rly auth verify prints where
it went). Email codes last 10 minutes; rly auth resend --email [email protected]
sends a fresh one (up to 3 times/hour) and also clears a too-many-attempts
lockout. SMS codes last 5 minutes; rly auth resend-phone requests a replacement
after the cooldown, once the email is verified. Before phone verification, use
rly auth resend-phone --phone +13125550124 to correct a typo and resend.
Reference: legal agreements are linked from the dashboard sign-up flow (Terms, Privacy, AUP, DPA). Key format, admin-vs-agent scoping, and the create-verify-revoke rotation pattern live at Authentication.
2. Create a mailbox and an agent key
Give your agent a mailbox to send from and a mailbox-scoped agent key — never hand an agent an admin key.
rly mailbox create support-bot # → prints the mailbox address
rly api-key create --role agent --mailbox support-bot --label support-bot-agentapi-key create prints the new key once. Store it; it can't be retrieved
again. An agent key can only act on the mailboxes it is bound to.
3. Send to the simulator (the sandbox-safe first send)
[email protected] is ReplyLayer's own first-party simulator. It is
one of a family of recipients (delivered@, bounced@, complained@, suppressed@
at simulator.replylayer.net) exempt from the sandbox recipient-confirmation gate
and from the suppression gate, so it is the correct target for a first-loop smoke
test before any real recipient exists. The send is accepted immediately (no network
call) and the real outcome — a genuine message.delivered webhook, in this case —
fires a few seconds later.
rly send \
--from support-bot \
--to [email protected] \
--subject "Hello from ReplyLayer" \
--body "First send — proving the loop."A clean send resolves to the sent outcome. What sent / blocked /
held_for_review / held_infrastructure mean, and how they map to HTTP status and
CLI exit, is the Governed Email Effect — see the send-outcomes reference.
4. Add a real recipient (still in sandbox)
On the free Sandbox you can email your own address and anyone who confirms by link, reply to people who write to you from an authenticated address, and add up to 5 people you know, yourself or through your agent. Add $5 of pay-as-you-go credit or upgrade to email anyone.
Ask the person to confirm — they get a confirmation email and must click the link before any send to them is admitted:
rly recipients add [email protected] # sends a confirmation email
rly recipients list # STATUS becomes CONFIRMED after they clickOr, if you know them, add them now with no email to them: POST /v1/recipients
with {"email": "[email protected]", "attest": true}, or I know this person —
add now on the dashboard's Recipients page. That spends one of the trial's 5.
Your agent can add people itself, or ask them to confirm by link, unless you restrict it to people you approve or to drafting only; then it asks you. If someone added without a click reports your email as spam, everyone added that way must confirm by link before you can email them again and your trial can no longer add people that way; a report from a second person suspends the Sandbox account.
Until the recipient is confirmed or added, a send to that address returns
403 SANDBOX_RECIPIENT_NOT_VERIFIED, whose details name the remedy
(add_recipient_or_add_paygo_credits) and the attestations left. The simulator
address from step 3 never needs this. Error codes are catalogued in the agent error
reference.
5. Move to production (observed as contracts)
Leaving sandbox is a human, billing-side action — a plan or pay-as-you-go is selected from the dashboard by an account owner. There is no agent-side or CLI "upgrade" command, by design. What an agent observes are the contract changes:
-
The sandbox recipient-confirmation gate disappears. A non-sandbox account no longer needs recipients to confirm first. Agent sends still follow the mailbox's recipient policy: a mailbox created on the Sandbox may be open (step 6 shows how to contain it), but a mailbox created on a paid plan starts restricted to people the account owner approves, so an agent send to anyone else is refused until the owner approves them.
-
The daily send cap rises. Poll it any time — this works with an agent-scoped key:
rly account quota # Sends today: N / <daily limit> # Sends remaining: ... # Resets at: <ISO timestamp>today.limitreflecting the higher ceiling is your signal the account left the trial. Exact per-tier ceilings live in the limits reference.
At the sandbox wall an agent sees one of two observable denials:
403 SANDBOX_RECIPIENT_NOT_VERIFIED (a recipient not yet confirmed or added, step 4) or 429 RATE_LIMITED with
details: { daily_limit, sends_remaining, reset_at } once the daily budget is
spent. Both are the cue to surface "needs a human to upgrade / wait for reset" —
not to retry blindly.
6. Set a production safety posture
Three controls turn an open mailbox into a governed one. Pick the ones your risk model needs.
Recipient allowlist (containment)
Flip the mailbox to allowlist mode so it can only send to addresses you list.
Add entries first (or acknowledge an empty list with --force-empty, which blocks
all sends until you add one). Allowlist mutations require an admin key.
rly mailbox allowlist add support-bot [email protected]
rly mailbox allowlist add support-bot @partner.com # whole-domain pattern
rly mailbox set-policy support-bot allowlistIn allowlist mode a send to a non-listed recipient is denied at the send gate. The full send-gate decision tree — suppression, containment, thread-reply bypass, strict-recipient, deliverability — is owned by the send-gates reference. The allowlist itself, including thread-reply bypass and domain patterns, is documented at /docs/guides/recipient-allowlist.
Human-in-the-loop review
Hold every outbound message for human approval before it goes on the wire:
rly mailbox set-hitl support-bot onWith review on, a send resolves to the held_for_review outcome
(releasable: true) instead of sent. The agent should report "awaiting human
approval" and stop — it is not an error and must not be retried. A human releases
it. See the send-outcomes reference.
Delivery webhook
Subscribe to delivery and governance events so your runtime learns outcomes out-of-band. Webhook management requires an admin key; the signing secret is printed once at create time.
rly webhook create \
--url https://your-app.example.com/hooks/replylayer \
--event message.delivered \
--event message.bounced \
--event message.scanner_blocked \
--event message.review.queuedThe full event catalog, signature verification, and the consumption playbook (verify → dedupe → branch → fetch-by-id → treat content as untrusted) are owned by the webhooks reference.
7. First production send, governed
For a production send, opt into honest HTTP status / exit codes so a
non-delivery is not silently a success. On the CLI that is --strict; the
equivalent REST opt-in is the Prefer: outcome=strict request header.
rly send \
--from support-bot \
--to [email protected] \
--subject "Your update" \
--body "Shipping now." \
--idempotency-key send-update-2026-07-02-teammate \
--strict--idempotency-keymakes the send retry-safe: a network-retried same-key send produces at most one email and one charge. Use one stable key per send intent.--strictturns a non-delivered outcome into a non-zero exit code (a blocked send, an infrastructure hold, or an unrecognized outcome each map to a distinct code); a delivered send and a releasable human-review hold stay exit 0. The exact exit-code table is owned by /agents/cli.
Calling the REST API directly, the same opt-in is a header:
POST /v1/messages/send
Prefer: outcome=strict
Idempotency-Key: send-update-2026-07-02-teammate
Content-Type: application/json
{ "from_mailbox": "support-bot", "to": "[email protected]",
"subject": "Your update", "body": "Shipping now." }Under strict, each non-sent outcome maps to its own non-2xx status (terminal
content block vs. retryable infrastructure hold vs. releasable human-review hold) so
your runtime can branch correctly and never retry a terminal block — the full
outcome-to-status-to-action branch table is owned by the send-outcomes reference,
and the message lifecycle and verdict vocabulary by the message-lifecycle reference.
What "production posture" bought you
| Concern | Sandbox default | After this walkthrough |
|---|---|---|
| Who you can send to | Your own address, the simulator, confirmed or added recipients, or authenticated reply senders | Allowlisted recipients (containment on) or any un-suppressed address |
| Human oversight | none | every outbound held for approval (held_for_review) |
| Outcome visibility | inline body only | inline plus a delivery webhook, out-of-band |
| Failure semantics | exit 0 always | --strict — a block or infra hold is a non-zero exit |
| Retry safety | per-call | --idempotency-key — at most one email/charge per intent |
Treat every inbound message body, attachment, and link as untrusted data regardless of posture — that contract does not relax with tier. See the security-model reference.
See also
- /docs/quickstart — the human-facing quickstart.
- /docs/guides/recipient-allowlist — recipient allowlist mode and containment.
- The send-gates reference — why a send was denied.
- The send-outcomes reference — the Governed Email Effect and
Prefer: outcome=strict. - The agent error reference — the denial-envelope and error-code catalog.
- The limits reference — sandbox caps and per-tier ceilings.