> ## 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 and test mode: build end to end without carrier spend

> Run your full integration against Orbit sandbox — magic-number sends with canned receipts, test numbers, simulated inbound messages, deterministic verification codes, and a go-live gate.

# Sandbox and Test Mode

Orbit sandbox is a complete simulation environment. Your integration code
runs unchanged against the real API — the same endpoints, the same request
shapes, the same webhook format — and Orbit short-circuits every send,
purchase, and verify call before it touches a carrier. Nothing bills, no
real phone rings, no audit rows you need to clean up later.

Sandbox mode activates when your request uses **any** of:

1. A sandbox API key (prefix `dv_test_sk_`), **or**
2. An organisation flagged `is_sandbox`, **or**
3. The `X-Test-Mode: true` header on a dashboard session.

Everything below is keyed off that signal. When you flip to a live key
(`dv_live_sk_`), the same request bodies hit production paths.

## Simulate sends and receipts with magic numbers

POST to the Messages API aiming at a magic test number, and Orbit returns
a deterministic delivery receipt (DLR). The trailing digit of the
recipient picks the scenario — `2` delivered, `3` undelivered, `7`
rejected — so you can drive every retry, dead-letter, and alerting branch
you have.

```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": "+15550100",
    "body": "delivered scenario"
  }'
```

The receipt sequence arrives as `message.status` webhooks to your
sandbox webhook URL, tagged `sandbox: true`. The full digit-to-scenario
table lives at [Sandbox magic numbers](/sandbox/magic-numbers).

## Buy test numbers without touching a carrier

`POST /api/v1/numbers/purchase` with a sandbox credential returns a
deterministic fictional number from the reserved `+1 555 01XX` range.
There is no provider call, no wallet charge, and the number is marked
`provider: "sandbox"` in your tenant inventory.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/numbers/purchase \
  -H "X-API-Key: $ORBIT_SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "+15550100",
    "country_code": "US"
  }'
```

Response:

```json theme={null}
{
  "data": {
    "id": "num_…",
    "phone_number": "+15550100",
    "status": "active",
    "provider": "sandbox",
    "monthly_cost": "0.0000"
  }
}
```

Use these sandbox-only numbers as the `from` in your send tests; the
trailing-digit table then drives the recipient-side outcome.

## Inject a simulated inbound message

To test the receive side — auto-responders, STOP keywords, inbox
routing — you don't need to hand-forge a provider webhook. POST a
synthetic inbound (mobile-originated) message into your sandbox inbox:

```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"
  }'
```

The message lands in your sandbox inbox like a real reply: the contact,
conversation, and message rows are created, the unassigned-conversation
notification fires, and your inbox view refreshes. This endpoint refuses
any request that is not in sandbox mode with `403 SANDBOX_ONLY`, so a
live inbox can never be polluted with synthetic traffic.

## Deterministic verification codes

The Verify API honours `custom_code` **only** on sandbox credentials, so
your QA and CI flows can assert a fixed OTP end to end without parsing a
real message.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/verify/send \
  -H "X-API-Key: $ORBIT_SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+15005550002",
    "channel": "sms",
    "custom_code": "123456"
  }'
```

Then check it:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/verify/check \
  -H "X-API-Key: $ORBIT_SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+15005550002",
    "code": "123456"
  }'
```

In live mode `custom_code` returns `403 CUSTOM_CODE_FORBIDDEN` — pinning
the OTP is a sandbox privilege, never a production one. A live verify
send still generates a random code server-side.

## Reset and re-seed the sandbox

When a test run leaves debris, reset the whole sandbox workspace or seed
deterministic fixtures:

| Endpoint                                      | Effect                                                                |
| --------------------------------------------- | --------------------------------------------------------------------- |
| `POST /api/v1/sandbox/reset`                  | Wipes contacts, conversations, and messages; releases sandbox numbers |
| `POST /api/v1/sandbox/provision-numbers`      | Bulk-provisions deterministic `+1 555 01XX` numbers                   |
| `POST /api/v1/sandbox/spawn-fixture-contacts` | Creates fixture contacts (optionally with conversations)              |

All three reject non-sandbox requests with `403 SANDBOX_ONLY` before a
single row changes.

## Pre-launch gate

Before go-live, Orbit evaluates a shared checklist against your org —
callable from either the live or the sandbox context, with the same
result either way:

```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,
    "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" }
    ]
  }
}
```

`readyForLaunch` flips to `true` when every item is complete. Wire it
into your launch script to fail CI on an unfinished gate, then walk the
human sign-off in the [production go-live checklist](/guides/go-live-checklist).

## Graduating to production

1. Finish the [pre-launch checklist](/sandbox/pre-launch-checklist).
2. Complete KYC and fund your balance — these two unlock live sending.
3. Mint a live key (`dv_live_sk_`) under **Settings → API Keys**.
4. Walk the [production go-live checklist](/guides/go-live-checklist)
   for numbers, webhooks, guardrails, and compliance.

Your code needs no changes — the sandbox signal comes from the key, not
from request bodies.
