> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Sandbox Magic Numbers

> Recipient trailing-digit convention that drives sandbox-mode delivery outcomes in Orbit

# Sandbox Magic Numbers

Orbit's sandbox simulator routes every send through a deterministic
rule keyed by the **trailing digit** of the recipient number, so you can
exercise your delivery-receipt (DLR), retry, and dead-letter handling
without touching a real carrier.

Every send is keyed by the **trailing digit** of the recipient number.
Send to `+15005550005` and you receive an `expired` delivery receipt; send
to `+15005550003` and you receive `undelivered`; send to `+15005550001`
and the carrier only ACKs the submit and never reports a final state — so
you can exercise your DLR-timeout handling. Each scenario walks the same
intermediate→terminal sequence a real carrier produces.

The simulator only activates when:

1. The API key is a sandbox key (prefix `dv_test_sk_*`), **or**
2. The organisation has `is_sandbox = true` in the Orbit data plane, **or**
3. The request carries the `X-Test-Mode: true` header from the
   dashboard.

Magic numbers in **live** mode are sent to the real carrier — there is
no production-side magic behaviour. The trailing-digit convention is
sandbox-only.

## Message sends — the delivery-state table

The trailing digit of the recipient selects one of ten delivery
scenarios. Each scenario emits its full delivery-receipt sequence as
`message.status` webhooks — an intermediate `sent` acknowledgement (when
the message reaches the carrier) followed by the terminal outcome:

| Trailing digit | Scenario               | Webhook `data.status` sequence | Terminal DLR latency |
| -------------- | ---------------------- | ------------------------------ | -------------------- |
| `1`            | Submitted only         | `sent`                         | never (ACK only)     |
| `2`            | Delivered              | `sent` → `delivered`           | \~50 ms              |
| `3`            | Undelivered            | `sent` → `undelivered`         | \~1150 ms            |
| `4`            | Failed (pre-submit)    | `failed`                       | \~100 ms             |
| `5`            | Expired (TTL)          | `sent` → `expired`             | \~3 s                |
| `6`            | Unknown (no report)    | `sent` → `unknown`             | \~5 s                |
| `7`            | Rejected (spam filter) | `rejected`                     | \~150 ms             |
| `8`            | Blocked (suppression)  | `blocked`                      | \~50 ms              |
| `9`            | Delayed                | `sent` → `delivered`           | \~60 s               |
| `0`            | Carrier rewrite        | `sent` → `delivered`           | \~50 ms              |

Each webhook carries a `state_class` (`intermediate` or `terminal`) and an
`is_terminal` flag so your handler can tell an in-flight update from a
final outcome — the same tags the production delivery-receipt path stamps.
The trailing-`0` (carrier rewrite) terminal receipt also carries
`body_rewritten_by_carrier: true`, mimicking regulatory sender-ID rewrites.

Delivery receipts do **not** change your stored message — a sandbox send
never reaches a carrier, so the message keeps its honest `test_sent`
status rather than being promoted to `delivered`/`failed`. The receipts
appear only in the webhook payloads sent to your sandbox webhook URL,
never on the message you retrieve through the API.

Any E.164 number with the same trailing digit triggers the same
scenario — `+15005550009`, `+15555550009`, and `+908505550009` all
resolve to the delayed-then-delivered sequence.

### Example: simulate a failed delivery

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/sms \
  -H "X-API-Key: $ORBIT_SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+15005550004",
    "from": "+15005550101",
    "body": "Trailing 4 → pre-submit failure, status=failed within ~100ms"
  }'
```

Your sandbox webhook endpoint receives a single `message.status` event
with `data.status: "failed"`, `state_class: "terminal"` — no real carrier
is touched, no balance is deducted.

### Example: simulate a delivered message

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/sms \
  -H "X-API-Key: $ORBIT_SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+15005550002",
    "from": "+15005550101",
    "body": "Trailing 2 → sent then delivered within ~100ms"
  }'
```

Your webhook receives two `message.status` events in order: an
intermediate `data.status: "sent"` (`state_class: "intermediate"`), then
`data.status: "delivered"` (`state_class: "terminal"`).

## Voice calls

<Warning>
  **Sandbox voice calls always resolve `completed` today.** The
  trailing-digit table below is a **reference catalog** of the outcomes
  `simulateCall` can compute — it is **not yet wired into the live call
  path**. The sandbox short-circuit pins the call record and the async
  lifecycle event to `completed`, and returns `{ "status": "completed" }`
  synchronously, regardless of the recipient's trailing digit. If you
  place a sandbox call to a number ending in `9` expecting `no-answer`,
  you will still receive `completed`. Treat the table below as the
  planned mapping, not behaviour you can trigger today.
</Warning>

`simulateCall` keys on the trailing digit too, but with three outcomes:

| Trailing digit | Outcome     |
| -------------- | ----------- |
| `9`            | `no-answer` |
| `8`            | `busy`      |
| any other      | `completed` |

The simulated lifecycle (`call.initiated` → `call.answered` →
`call.ended`) fires through the same `sandbox-events` worker.

## Reference catalog: `GET /sandbox/numbers`

Orbit also publishes a **10-state magic-number catalog** for clients that
want to render their own developer surface (e.g. an embedded "Test your
integration" widget). This is a **descriptive reference table** returned
by the read-only `GET /sandbox/numbers` endpoint and serialised from
`SANDBOX_MAGIC_NUMBERS` in `@devotel/messaging/sandbox/magic-numbers`.

<Note>
  These ten states are the same scenarios the live send/DLR path fires
  (see the delivery-state table above). The catalog is a
  machine-readable copy of that mapping so you can render your own
  developer surface — e.g. an embedded "Test your integration" widget —
  without hard-coding the digits and latency bands yourself.
</Note>

```bash theme={null}
curl https://api.orbit.devotel.io/api/v1/sandbox/numbers \
  -H "X-API-Key: $ORBIT_SANDBOX_KEY"
```

Response (one entry per trailing digit):

```json theme={null}
{
  "data": {
    "numbers": [
      {
        "trailing_digit": "1",
        "example_e164": "+15005550001",
        "status": "submitted",
        "label": "Submitted only",
        "description": "Carrier accepts the message and returns an ACK, but never reports a final delivery state...",
        "fires_dlr": false,
        "dlr_latency_ms_min": 0,
        "dlr_latency_ms_max": 0,
        "rewrite_footer": null
      }
    ],
    "docs_url": "https://docs.orbit.devotel.io/sandbox/magic-numbers"
  }
}
```

The full catalog covers the statuses `submitted`, `delivered`,
`undelivered`, `failed`, `expired`, `unknown`, `rejected`, `blocked`,
`delayed`, and `carrier_rewrite`, each keyed by trailing digit
(`1`–`0`). See `SANDBOX_MAGIC_NUMBERS` for the canonical example numbers,
latency bands, and descriptions.

## Hard rules

* **No real carrier is ever reached.** Every magic-number send
  short-circuits before the provider router resolves a backend.
* **No balance is deducted.** Sandbox sends always have
  `cost_usd_cents: 0` and never reach Stripe or your wallet.
* **DLRs fire to your sandbox webhook URL only.** Configure this
  separately from your live URL under **Settings → API Keys →
  Sandbox** so production handlers never see test traffic.
* **Channels covered: SMS, MMS, WhatsApp, RCS, Viber, Telegram,
  Messenger, AMB, Email.** Channel-specific status codes (e.g.
  WhatsApp `131047`) are out of scope for the magic-number convention;
  the trailing-digit rule maps to the canonical generic status set.
