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.
- Verify a real recipient — the sandbox's containment model.
- 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 SMSEmail 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. 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. Verify a real recipient (still in sandbox)
To send to a real person while still in sandbox, that address must confirm first. Add it — the recipient gets 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 clickUntil the recipient confirms, a send to that address returns 403 FORBIDDEN
("Sandbox accounts can only send to confirmed recipients."). 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 can send to any un-suppressed recipient without pre-confirming it (subject to any containment posture you opt into in step 6).
-
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 FORBIDDEN
(unconfirmed recipient, 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 | Confirmed recipients + the simulator only | 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.