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:

  1. Sign up and verify your email and mobile number.
  2. Send to the simulator — the one address that skips the sandbox recipient gate.
  3. Verify a real recipient — the sandbox's containment model.
  4. Move to production — what changes, observed as contracts (not internals).
  5. Set a safety posture — allowlist mode, human review, a delivery webhook.
  6. First production send with --strict so 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_xxxxxxxx

On 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 SMS

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. 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-agent

api-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 click

Until 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.limit reflecting 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 allowlist

In 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 on

With 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.queued

The 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-key makes 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.
  • --strict turns 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

ConcernSandbox defaultAfter this walkthrough
Who you can send toConfirmed recipients + the simulator onlyAllowlisted recipients (containment on) or any un-suppressed address
Human oversightnoneevery outbound held for approval (held_for_review)
Outcome visibilityinline body onlyinline plus a delivery webhook, out-of-band
Failure semanticsexit 0 always--strict — a block or infra hold is a non-zero exit
Retry safetyper-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