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.

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 referenceTENANT_PROVISIONING and TENANT_SCHEMA_INCOMPLETE semantics.