Skip to main content

Sandbox and Test Mode

Every endpoint page in the API reference lists the same X-Test-Mode header parameter. This guide is the authoritative explanation of what it does: when test mode applies, how the header and your credential interact, and worked samples for every auth type. It then walks the full sandbox flow — magic-number sends, test numbers, simulated inbound messages, deterministic verify codes, and the go-live gate.

When test-mode applies

Test mode is resolved per request, before any route handler runs, from your credential and the X-Test-Mode header:
  • Session-authenticated (Clerk) requests. A browser signed into the dashboard sends session auth — the dashboard’s Test mode toggle in the topbar works this way, and it stamps X-Test-Mode: true on every call it makes. For session auth the header is honored: true opts that request into test mode, absent or false runs it live. This is the only auth type the header can influence.
  • API key auth. A live key (dv_live_sk_*) is always live; a test key (dv_test_sk_*) is always sandbox. The prefix of the key you minted under Settings → API Keys decides — request headers cannot override it.
  • Sandbox orgs. An org flagged as a sandbox org resolves every credential on it to test mode, regardless of key prefix or header.

The routing matrix

The header is honored only when there is no API key in play — a partner or reseller holding your live key cannot flip it into sandbox from the wire, and a live dashboard session can sandbox itself without touching your shared integration credentials. When a request engages test mode, the response envelope carries meta.test_mode: true; assert that marker rather than trusting the header you sent or the key prefix you pulled from config.

Session auth: Clerk only

“Session-authenticated” means the caller authenticated with a Clerk browser-session token and sent no API key — the auth model the dashboard itself uses, in this authority order:
  1. API key auth wins whenever a key (dv_live_sk_* or dv_test_sk_*) is present — the key prefix decides the mode.
  2. Only when no key is attached does a Clerk session authenticate the request, and only then is X-Test-Mode: true read at all.
The header does nothing for an API-key-authenticated request even when that request also carries a valid session cookie — the key path already resolved authority before the header is looked at. A mixed request behaves exactly like the key alone.

Worked samples per auth type

Live key — header does not sandbox it

Sending X-Test-Mode: true with a live key still hits the provider and still charges your balance. The response envelope shows no test_mode marker:
This safety rule is what keeps a leaked or third-party-held live key from reaching the shared sandbox sender pool. To sandbox from a script or server, use a test key.

Test key — unconditionally sandbox

A dv_test_sk_* key engages test mode on every request, with or without the header:
The message terminates at test_sent, no provider is called, no balance moves. Add X-Test-Mode: false to the same request and the result is identical — the key prefix already decided.

Session auth with the toggle on — header decides

From a Clerk session — authenticated cookie-style by the browser, no API key attached — the header is the switch. With it on, the call sandboxes exactly like a test key:
The dashboard’s own browser calls authenticate this way; its Test mode toggle stamps the header on every one of them.

Session auth with the toggle off — provider delivery

With the header absent or false, the same session-authenticated call runs live:

Common pitfalls

  1. Retrying a live key with X-Test-Mode: true. The header is ignored on key auth, so the retry still delivers and still charges. The fix is a dv_test_sk_* key, not a header value sweep. Symptom: the response keeps coming back with the live queued status and no meta.test_mode marker.
  2. A dashboard session that never goes live. If a colleague left Test mode on in the dashboard, every browser call sandboxes silently — sends look healthy (test_sent) but no recipient ever receives one. Check the topbar toggle before checking the number.
  3. Asserting the header or key prefix instead of the marker. Config drift between environments means “we sent the test key” and “the request sandboxed” disagree eventually. Gate the assertion on meta.test_mode: true in the response; it is the only reliable signal.
  4. Setting metadata.test_mode: true in a request body. That field is an output stamp the platform writes, not an input — the request remains live and your body field is stored verbatim.

What the sandbox gives you once it is on

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. Everything below is keyed off the same signal defined above. 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.
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.

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.
Response:
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:
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.
Then check it:
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: 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:
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.

Graduating to production

  1. Finish the 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 for numbers, webhooks, guardrails, and compliance.
Your code needs no changes — the sandbox signal comes from the key, not from request bodies.

Cross-references