> ## 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 API: worked examples — provision, fixtures, Virtual Phone, reset

> Copy-pasteable curl recipes for every sandbox route — activate test mode, provision test numbers, spawn fixture contacts, drive the Virtual Phone, read magic numbers and the pre-launch gate, and reset.

# Sandbox API Cookbook

Every sandbox route the [sandbox overview](/sandbox/overview) catalogs, shown
as a runnable request/response pair. The [sandbox and test
mode](/guides/sandbox-test-mode) guide walks the concepts end to end; this
page is the recipe sheet you keep open while you script against the sandbox.

All examples use your sandbox secret key, exported as an environment
variable:

```bash theme={null}
export ORBIT_SANDBOX_KEY="dv_test_sk_…"
```

## 1. Setup — what puts a request in sandbox mode

Any one of these three signals activates sandbox mode; the mutation routes
below gate on them before touching data. You usually need exactly one.

**Signal A — a sandbox API key** (the normal one for scripts):

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

**Signal B — a sandbox organization.** Every account ships with a paired
sandbox org; pick it in the console organization switcher and an API key
minted on that org is implicitly sandbox-scoped.

**Signal C — the `X-Test-Mode: true` header** (dashboard sessions only —
the Test/Live toggle sets it for you):

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

GET endpoints like `/sandbox/numbers` and `/sandbox/pre-launch-checklist`
do not gate on the signal (any authenticated key can read them), but every
POST below refuses a non-sandbox context with `403 SANDBOX_ONLY`. See the
[failure modes](#8-failure-modes) section for the envelope.

## 2. Provision test numbers

Mint deterministic fictional `+1 (555) 01XX` numbers into your sandbox
inventory. No carrier is touched — the rows write straight into your
tenant with `provider: "sandbox"` and `monthly_cost: "0.0000"`.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/sandbox/provision-numbers \
  -H "X-API-Key: $ORBIT_SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "count": 5 }'
```

```json theme={null}
{
  "data": {
    "count": 5,
    "numbers": [
      {
        "id": "number_…",
        "phone_number": "+15550137",
        "country_code": "US",
        "capabilities": ["sms", "voice"],
        "provider": "sandbox",
        "number_type": "local",
        "status": "active",
        "monthly_cost": "0.0000"
      }
    ]
  },
  "meta": { "request_id": "…", "timestamp": "…" }
}
```

Bounds and behaviour:

* `count` caps at **25 per call** (`MAX_PROVISION_NUMBERS`); `country_code`
  (2-letter, default `US`) and `capabilities` (a non-empty subset of
  `sms` / `voice` / `mms`) are optional.
* The numbers are **deterministic per tenant** — a reset-then-provision
  run gets the identical inventory, and re-provisioning the same count is
  idempotent (the deterministic number is the conflict key, so it upserts
  rather than duplicates).

Rejections: `count` above 25 (or `0`, or non-integer) fails validation
with a `400`; an omitted body defaults `count` to 5.

## 3. Spawn fixture contacts

Seed deterministic contacts (Ada Lovelace, Grace Hopper, …) so your inbox,
auto-responder, and segment flows have something to work against. Each
fixture's phone ends in the trailing digit of one of the 10 [magic
numbers](/sandbox/magic-numbers), so every delivery scenario has a
ready-made recipient.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/sandbox/spawn-fixture-contacts \
  -H "X-API-Key: $ORBIT_SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "count": 3, "with_conversations": true }'
```

```json theme={null}
{
  "data": {
    "conversations_created": 3,
    "contacts": [
      {
        "id": "contact_…",
        "first_name": "Ada",
        "last_name": "Lovelace",
        "display_name": "Ada Lovelace",
        "email": "ada.lovelace.…@sandbox.orbit.test",
        "phone": "+15005550001",
        "company": "Analytical Engines",
        "lifecycle_stage": "lead",
        "tags": ["sandbox-fixture"]
      }
    ]
  },
  "meta": { "request_id": "…", "timestamp": "…" }
}
```

Bounds and behaviour:

* `count` caps at **50 per call** (`MAX_FIXTURE_CONTACTS`); an omitted body
  defaults to 10 contacts.
* `with_conversations` defaults to `true`. With it on, each contact gets
  one open inbound SMS conversation carrying a greeting message, so the
  **Inbox** has a live thread immediately. Set it to `false` when you want
  bare contacts.
* Fixture contacts are tagged `sandbox-fixture`, which is what
  `POST /sandbox/reset` sweeps away with the rest of the workspace.

## 4. Virtual Phone — inject an inbound message

Simulate the handset *replying* to you: the message lands in your sandbox
inbox exactly like a real inbound provider webhook, without forging one.
Use it to drive auto-responders, STOP keywords, inbox routing, and the
unassigned-notification path.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/sandbox/inbound \
  -H "X-API-Key: $ORBIT_SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "+15005550002",
    "channel": "sms",
    "body": "STOP",
    "contact_name": "QA Handset"
  }'
```

```json theme={null}
{
  "data": {
    "message_id": "msg_…",
    "conversation_id": "conv_…",
    "contact_id": "contact_…",
    "contact_name": "QA Handset",
    "channel": "sms",
    "from": "+15005550002",
    "to": "sandbox",
    "body": "STOP",
    "media_url": null,
    "status": "received",
    "created_contact": true,
    "created_conversation": true
  },
  "meta": { "request_id": "…", "timestamp": "…" }
}
```

Request rules:

* `channel` accepts only `sms`, `mms`, or `whatsapp` (default `sms`).
* The call is rejected (`400 VALIDATION_ERROR`) unless it carries a
  non-empty `body` **or** a `media_url` (an `https://` URL, max 2048
  chars, for an MMS / WhatsApp media message). `body` caps at 1600
  characters.
* `from` must be E.164; `to` is optional (defaults to the `sandbox`
  sentinel). `contact_name` sets the display name on a freshly created
  contact.
* Repeat injections from the same sender on the same channel thread into
  the open conversation (`created_conversation: false`), matching the
  real inbound path.

The response's `created_contact` / `created_conversation` flags tell you
whether this injection minted rows or threaded into existing ones — the
same distinction the live recorder reports.

## 5. Reset the sandbox

Wipe the sandbox workspace back to a clean slate in one transaction —
every contact, conversation, and message, plus every sandbox-provisioned
number released. Your API keys, saved test recipient, and sandbox webhook
URL survive (they are org settings, not workspace data).

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

```json theme={null}
{
  "data": {
    "contacts_deleted": 12,
    "conversations_deleted": 7,
    "messages_deleted": 19,
    "numbers_released": 5
  },
  "meta": { "request_id": "…", "timestamp": "…" }
}
```

Zero counts mean the workspace was already clean — reset reports what it
actually deleted. Real-provider numbers (which a sandbox workspace should
never hold) are untouched.

## 6. Magic numbers catalog

The 10 delivery-state scenarios, keyed by the recipient's trailing digit.
Any authenticated key reads it; the dashboard's magic-number table renders
exactly this list.

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

```json theme={null}
{
  "data": {
    "docs_url": "https://docs.orbit.devotel.io/sandbox/magic-numbers",
    "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
      },
      {
        "trailing_digit": "2",
        "example_e164": "+15005550002",
        "status": "delivered",
        "label": "Delivered (happy path)",
        "description": "Carrier accepts and the handset reports delivered within ~50 ms.",
        "fires_dlr": true,
        "dlr_latency_ms_min": 0,
        "dlr_latency_ms_max": 100,
        "rewrite_footer": null
      }
    ]
  },
  "meta": { "request_id": "…", "timestamp": "…" }
}
```

Target sends at these numbers to drive your retry, dead-letter, and
alerting branches; the full digit-to-scenario semantics live at [magic
numbers](/sandbox/magic-numbers).

## 7. Pre-launch checklist

The shared five-step go-live gate — callable with either a live or sandbox
key, and always evaluated against your **live** workspace so both contexts
agree on the result.

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

```json theme={null}
{
  "data": {
    "readyForLaunch": false,
    "isSandbox": true,
    "sandboxPairId": "org_…",
    "items": [
      { "id": "sandbox_workspace_ready", "status": "complete" },
      { "id": "live_workspace_ready", "status": "pending" },
      { "id": "sandbox_token_minted", "status": "complete" },
      { "id": "integration_exercised", "status": "complete" },
      { "id": "ip_allowlist_configured", "status": "pending" }
    ]
  },
  "meta": { "request_id": "…", "timestamp": "…" }
}
```

Parse `readyForLaunch` — it flips to `true` only when every item reports
`status: "complete"`. Wire it into launch scripts to fail CI on an
unfinished gate; the [pre-launch checklist
reference](/sandbox/pre-launch-checklist) explains each item.

## 8. Failure modes

The gate every mutation route enforces — shown here with a **live** key:

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

```json theme={null}
{
  "error": {
    "code": "SANDBOX_ONLY",
    "message": "Sandbox state-control endpoints require a sandbox API key (dv_test_sk_…) or a sandbox organization. Refusing to mutate a live tenant.",
    "status": 403
  },
  "meta": { "request_id": "…", "timestamp": "…" }
}
```

Other errors you will hit, and what they mean:

| HTTP  | Code               | Cause                                                                                                                           |
| ----- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| `403` | `SANDBOX_ONLY`     | A POST ran in a live context — activate one of the three setup signals                                                          |
| `400` | `VALIDATION_ERROR` | Bad body: `count` out of range, non-E.164 `from`, missing `body` + `media_url`, bad `channel`                                   |
| `401` | `UNAUTHORIZED`     | Missing or invalid API key                                                                                                      |
| `429` | `RATE_LIMITED`     | Over the 60 req/min per-tenant authenticated-write budget (all sandbox mutations share it) — back off to the next minute window |

## Honest limits

* **Number inventory is fictional.** Provisioned numbers come from the
  NANP-reserved `+1 (555) 01XX` range and carry `provider: "sandbox"` —
  they can send to and receive from other sandbox fixtures only. No
  carrier ever sees them.
* **Channels that still run live.** Only SMS, MMS, WhatsApp, and Email
  short-circuit in test mode. **Voice, RCS, push, and fax still hit their
  real stacks and bill normally** even with a sandbox key — route those
  tests at `+1 555` numbers to avoid carrier fees, and expect no sandbox
  `inbound` support for them (the endpoint accepts only the three
  messaging channels above).
* **Sandbox POSTs write real rows into your sandbox tenant.** Reset is
  the only cleanup — there is no per-route undo.
* **Rate budget is shared.** Console clicks on the **Developer →
  Sandbox** page and your scripts draw from the same 60 req/min write
  limit.

## Related pages

* [Sandbox overview](/sandbox/overview) — the concepts, activation
  signals, and constants this cookbook exercises
* [Sandbox and test mode](/guides/sandbox-test-mode) — the end-to-end
  walkthrough including magic-number sends and deterministic verify codes
* [Sandbox dashboard walkthrough](/guides/sandbox-dashboard-walkthrough) —
  the console equivalent of every recipe here
* [Magic numbers](/sandbox/magic-numbers) — the 10 delivery scenarios in
  full
* [Pre-launch checklist](/sandbox/pre-launch-checklist) — the five-step
  gate, item by item
* [Developer API playground](/guides/developer-api-playground) — run these
  calls from the in-console request console instead of curl
