> ## 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, test mode, and the provisioning model

> How Orbit separates test traffic from production traffic and from an unprovisioned org: the three ways test mode switches on, what a test send does and does not touch, how to keep sandbox out of production metrics and webhooks, and what the TENANT_PROVISIONING 503 actually means.

# Sandbox, test mode, and provisioning

Two transient states look similar from the client side and mean
completely different things: an org whose database schema is still
provisioning returns 503 `TENANT_PROVISIONING`, and an org operating in
test mode returns 2xx with `meta.test_mode: true`. The first is a
lifecycle fact about the org itself; the second is a per-credential,
per-request decision you control. This page defines both, catalogues
what test mode does and does not touch, and lists the integration
mistakes that recur most.

## The test-mode switch: three ways in

Test mode is resolved on every request, before any handler runs. Three
inputs turn it on, and any one of them is sufficient:

1. **A test-prefixed API key.** A key beginning with `dv_test_sk_*`
   unconditionally enables test mode on every request it authenticates.
   This is the canonical switch for server-to-server integrations —
   rotate to a test key and every call through it is sandboxed.
2. **The `X-Test-Mode: true` header on a session-authenticated
   request.** The dashboard's in-topbar **Test mode** toggle sets this
   header on every call it makes. It is honored only for
   browser-session auth — a live API key (`dv_live_sk_*`) that sends
   `X-Test-Mode: true` gets **live mode anyway**; the header is ignored
   there so a partner or reseller key cannot flip into the shared
   sandbox sender pool from the wire. Server-to-server clients always
   use a `dv_test_sk_*` key instead.
3. **An org flagged as a sandbox org.** Every key minted on a sandbox
   org resolves to test mode regardless of key prefix or header; there
   is no way to send live from a sandbox org.

Live credentials can never reach test mode; test credentials can never
reach live providers. The separation is enforced at auth time, not by
discipline.

`metadata.test_mode: true` on a message send is the **output marker**,
not an input. Passing `metadata: { test_mode: true }` in a send body
does not sandbox the call — switch the credential or the header.

## What a test send does and does not touch

A test-mode send is written to your tenant database like any other send,
then short-circuits **before any provider dispatch**. Concretely:

* **It is persisted.** The row exists, with `metadata.test_mode: true`,
  so your integration can `GET /messages/:id` and exercise its retrieval
  code paths against it.
* **No provider sees it.** No SMPP submission, no WhatsApp/Meta handoff,
  no SMTP session. The carrier side is never reached.
* **No balance moves.** No wallet debit, no credit reservation, no usage
  charge. Ever.
* **It resolves to `test_sent`.** Delivery is simulated: the message
  terminates at the `test_sent` status, and the sandbox simulator can
  additionally synthesize a delivery-receipt sequence (delivered,
  failed, expired, undelivered — keyed by the recipient's trailing
  digit, see [Sandbox magic numbers](/sandbox/magic-numbers)) so your
  webhook handlers run against realistic transitions. Those simulated
  receipts appear only in webhook payloads; the stored message keeps its
  honest `test_sent` status.
* **Every response carries `meta.test_mode: true`**, so your client can
  assert sandbox dynamically rather than trusting environment naming.

## Which objects participate

Test mode is per-org (scoped to your tenant), not a separate
environment, and one class of objects plays no part in it:

* **Messages.** All channels (SMS, MMS, WhatsApp, RCS, email, and the
  rest) short-circuit the same way: persisted, never dispatched,
  `metadata.test_mode: true`, no charge.
* **Verify sessions.** Verification flows are the one object with an
  explicit trust boundary: a live credential **cannot** run a
  verification with a caller-supplied `custom_code` — that path
  requires `dv_test_sk_*` credentials (or `X-Test-Mode: true` on
  session auth), and the API tells you so. In live mode the platform
  always generates the OTP.
* **Webhook receivers.** Simulated lifecycle events for test sends (the
  magic-number DLR sequences, the `message.sent` event for the
  `test_sent` terminal state) are delivered to your org's **sandbox
  webhook URL** — a receiver you configure separately from the live
  one, under **Settings → API Keys → Sandbox** or via the
  `sandbox_webhook_url` organization setting. Properly configured, your
  production handler never sees test traffic at all. Every test-mode
  webhook payload carries `metadata.test_mode: true` as well, so a
  handler that ignores the two-URL split can still detect and drop test
  events programmatically.

## Keeping sandbox out of production

Two disciplines keep the two worlds separate on your side:

1. **Branch on `metadata.test_mode` for every event type, not just
   `message.sent`.** A test send emits the same event families a live
   send does — `message.sent` with `status: "test_sent"`, plus simulated
   delivery-receipt events if magic numbers are in play. A webhook
   handler that filters only one event type will let the others through.
   The right gate is an early `if (data.metadata?.test_mode === true)`
   return on all message events — or, better, separate delivery: the
   sandbox webhook URL means the event never reaches your production
   handler.
2. **Exclude `metadata.test_mode = true` rows from every metric you
   report.** Delivered-rate, failure-rate, cost, and throughput
   aggregations that count test rows overstate volume and distort
   outcome rates. The condition is cheap (`metadata->>'test_mode'` is
   null or absent on live rows); the mistake is expensive because it is
   silent and compounds over time.

## The provisioning context

Test mode decides whether a send is simulated; provisioning decides
whether the org can serve API traffic at all.

Every organization starts as a catalog row. Provisioning then creates
the org's tenant schema — the isolated per-org data store described in
[Tenant isolation](/concepts/tenant-isolation) — runs the tenant table
set against it, and finally flips the org's `tenant_schema_ready` flag.
While that flag is false, most endpoints return
`503 TENANT_PROVISIONING`.

That 503 is **transient onboarding state, not a client error.** The
integration lives in your code before the org's schema exists in the
database; retry with backoff and the same call succeeds once
provisioning completes. Endpoints that legitimately run pre-provisioning
(an onboarding status poll is the canonical case) opt out of the gate
explicitly.

Test mode and provisioning are orthogonal. They combine without special
rules: a key on a not-yet-provisioned org gets `TENANT_PROVISIONING`
whether it is live or test — provisioning finishes when you finish
onboarding, not when you pick a credential. A provisioned org serves
live and test traffic side by side, decided per request by the
credential or header.

## Common pitfalls

1. **Counting `test_sent` in delivered metrics.** `test_sent` is a
   terminal status that appears in list endpoints and metrics rows just
   like `delivered`. Exclude `metadata.test_mode = true` from every
   aggregation, or your delivered rate includes messages no carrier ever
   saw.
2. **Treating `TENANT_PROVISIONING` 503 as a real onboarding error.**
   It fires when an org row exists but its schema is not ready yet. It
   resolves itself; the only bug is surfacing it to the end user as a
   failure instead of retrying or showing an onboarding-in-progress
   state.
3. **Sending `X-Test-Mode: true` with a live API key and expecting
   sandbox.** The header is ignored on server-to-server auth. Requests
   remain live — real provider dispatch, real charges. Rotate to a
   `dv_test_sk_*` key.
4. **Setting `metadata.test_mode: true` in the request body and
   expecting sandbox.** It is an output marker the platform stamps, not
   an input. The request stays live.
5. **Pointing sandbox events at a production handler.** Either
   configure the separate sandbox webhook URL so test events never
   reach it, or branch on `metadata.test_mode` at the top of the
   handler — on every event type, not only `message.sent`.
6. **Provisioning a Verify integration with `custom_code` before
   switching to test credentials.** Live credentials cannot supply an
   OTP; generate the code server-side or move the integration to a
   `dv_test_sk_*` key first.

## Cross-references

* [Delivery lifecycle](/concepts/delivery-lifecycle) — where `test_sent`
  sits in the message state machine and which webhook events it emits.
* [Tenant isolation](/concepts/tenant-isolation) — the per-tenant schema
  that provisioning creates, and the request-resolution chain that
  returns `TENANT_PROVISIONING` while it is absent.
* [Sandbox magic numbers](/sandbox/magic-numbers) — simulate specific
  delivery-receipt sequences by recipient trailing digit.
* [Error codes reference](/reference/error-codes) — `TENANT_PROVISIONING`
  and `TENANT_SCHEMA_INCOMPLETE` semantics.
