# 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](/docs/limits) 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.

```bash
rly signup \
  --email you@example.com \
  --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:

```bash
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 you@example.com` 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](https://app.replylayer.ai/legal/terms),
[Privacy](https://app.replylayer.ai/legal/privacy),
[AUP](https://app.replylayer.ai/legal/acceptable-use),
[DPA](https://app.replylayer.ai/legal/dpa)). Key format, admin-vs-agent scoping,
and the create-verify-revoke rotation pattern live at [Authentication](/docs/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.

```bash
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)

`delivered@simulator.replylayer.net` 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.

```bash
rly send \
  --from support-bot \
  --to delivered@simulator.replylayer.net \
  --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](/agents/send-outcomes).

---

## 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:

```bash
rly recipients add teammate@example.com     # 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:

  ```bash
  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](/docs/limits).

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.

```bash
rly mailbox allowlist add support-bot teammate@example.com
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](/agents/send-gates). The
allowlist itself, including thread-reply bypass and domain patterns, is documented
at [/docs/guides/recipient-allowlist](/docs/guides/recipient-allowlist).

### Human-in-the-loop review

Hold every outbound message for human approval before it goes on the wire:

```bash
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](/agents/send-outcomes).

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

```bash
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](/agents/webhooks).

---

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

```bash
rly send \
  --from support-bot \
  --to teammate@example.com \
  --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](/agents/cli).

Calling the REST API directly, the same opt-in is a header:

```http
POST /v1/messages/send
Prefer: outcome=strict
Idempotency-Key: send-update-2026-07-02-teammate
Content-Type: application/json

{ "from_mailbox": "support-bot", "to": "teammate@example.com",
  "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](/agents/send-outcomes),
and the message lifecycle and verdict vocabulary by the [message-lifecycle reference](/agents/messages).

---

## 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](/docs/quickstart) — the human-facing quickstart.
- [/docs/guides/recipient-allowlist](/docs/guides/recipient-allowlist) — recipient allowlist mode and containment.
- The [send-gates reference](/agents/send-gates) — why a send was denied.
- The [send-outcomes reference](/agents/send-outcomes) — the Governed Email Effect and `Prefer: outcome=strict`.
- The [agent error reference](/agents/errors) — the denial-envelope and error-code catalog.
- The [limits reference](/docs/limits) — sandbox caps and per-tier ceilings.
