Skip to main content

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) 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.

Worked examples: the wire, the markers, the 503

Prose defines the contract; these snippets pin it. Each one is complete enough to paste into a test. The Sandbox and test mode guide walks the full flow (magic numbers, test numbers, go-live gate) — this page shows the response markers you assert against at each step.

Send in test mode

Authenticate with a dv_test_sk_* key (or send X-Test-Mode: true from an authenticated dashboard session) and POST a message exactly as you would live:
Two markers in the response prove it short-circuited: the message row terminates at the dedicated test_sent status, and the response envelope asserts meta.test_mode: true:
A live key (dv_live_sk_*) sends whether you pass X-Test-Mode: true or not — the two markers above are the only reliable signal that test mode actually engaged.

Read the test row back

Every test send persists, so your integration’s retrieval path has a real row to fetch — no mocks needed:
metadata.test_mode on the stored row is the stamp the API prints when it persists the message; it is what your metrics queries exclude, and — with the honest test_sent status — what keeps test rows from reading as real deliveries.

Branch on meta.test_mode

A webhook handler that gates on the marker is safe even when it ignores the sandbox-URL split. For a Node handler that successfully verified the payload, drop test events before any side effect:
Gate on every event type, not only message.sent: simulated delivery-receipt sequences and inbox events carry the same stamp and are the ones that slip through a single-event filter.

Assert in code

In CI, assert the marker rather than trusting the key prefix you used:
If the second assertion fails, the request was live: real provider dispatch, real wallet charge. Fix the credential before the test suite passes green again.

The provisioning 503

While the org’s tenant schema is still being created, most endpoints return 503 with the TENANT_PROVISIONING code. It is onboarding state, not an error in your integration — meta.request_id there is what support will ask for if it ever appears stuck:
Retry with backoff; the same call succeeds as soon as provisioning completes. Only onboarding-status polls opt out of the gate.

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 — 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 — where test_sent sits in the message state machine and which webhook events it emits.
  • 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 — simulate specific delivery-receipt sequences by recipient trailing digit.
  • Error codes reference — TENANT_PROVISIONING and TENANT_SCHEMA_INCOMPLETE semantics.
  • The channel test-account model — the shared-asset → BYO ladder on interactive channels (Telegram, WhatsApp): a real delivery path behind gates, orthogonal to this page’s simulated test mode.
  • Sandbox and test mode guide — walks the end-to-end flow (magic numbers, test numbers, simulated inbound, deterministic verify codes, go-live gate); the send curls live there — this page pins the response markers each curl produces.