Browse documentation

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. Add a real recipient — the sandbox's recipient rule.
  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

The 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-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. 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 click

Or, 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.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 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 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 toYour own address, the simulator, confirmed or added recipients, or authenticated reply sendersAllowlisted 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