Skip to main content

The sandbox model

Orbit sandbox works because it is a model, not a demo flag in each handler. At one point on the request path the platform stamps the request as test-mode; everything downstream of that stamp — routing, providers, events, billing — reads the stamp and behaves sandbox-safe. This page explains that model: where the boundary sits, how the synthetic provider family answers per channel, how data is isolated, and what carries over when you promote to production. The how-to walkthrough lives in the sandbox and test mode guide; this page is the concept behind it.

1. What sandbox is

Sandbox is a synthetic tenant context activated per request, resolved before any routing or provider decision. Three inputs activate it, any one sufficient: a dv_test_sk_* API key, an organization flagged as a sandbox org, or X-Test-Mode: true on a dashboard session. Live credentials resolve to live; test credentials resolve to sandbox; never both. The sandbox context runs the identical API surface with the same endpoints, request shapes, and webhook payload format. Two things distinguish it: requests route to a synthetic provider family instead of carrier dispatch, and event traffic carries distinct markersmetadata.test_mode: true on messages and events, provider: "sandbox" on numbers — so a reader or handler can always separate the two worlds.

2. Where the boundary sits

Sandbox mode is resolved on every request by middleware, before any route handler, routing table, or provider adapter runs. The stamp is an input decision evaluated once; downstream layers read it, they do not re-derive it. That ordering buys three properties:
  • Routing reads the stamp. A sandbox send short-circuits before the routing table picks a carrier. Provider adapters never see it.
  • Every response carries the marker. Envelopes say meta.test_mode: true, so asserting the sandbox boundary in code is a one-field check.
  • Outbound events carry the marker on every event type. Every event payload for sandbox traffic carries its test_mode flag — message.sent, simulated delivery receipts, inbox events — so a handler can gate even if it ignores the separate sandbox webhook URL. A dedicated sandbox webhook URL (under Settings → API Keys → Sandbox) delivers sandbox events to a separate receiver, so a production handler never sees synthetic traffic.
A synthetic fragment for a sandbox send:
The dedicated test_sent status and the meta.test_mode: true envelope field are the two markers that prove the boundary engaged; assert both in integration tests.

3. The synthetic provider family

Each channel has a sandbox provider that answers without touching a carrier. The response is canned and deterministic so tests stay reproducible: Every row in the table is persisted with the test marker, and every emitted event carries it — your webhook handlers and retrieval code run against real rows, while no carrier ever participates.

4. What you can and cannot do

You can, with no carrier traffic involved: exercise sends on all channels, drive every webhook path with simulated receipts, purchase fictional +1(555)01XX test numbers with provider: "sandbox", inject simulated inbound messages into a sandbox inbox, and pin deterministic verification codes (custom_code on the Verify API) for QA and CI. You cannot: reach a real carrier, ring a real phone, or hit Meta, SMPP, or SMTP. Live credentials do not join the sandbox context 403 CUSTOM_CODE_FORBIDDEN on custom_code, and the X-Test-Mode header is honored only for session auth — a live API key that sends it stays live. Sole exception: trial numbers are still real. Because they sit on the real inventory, a trial number can receive and place real traffic while the rest of the org is sandbox-only; treat the trial-number boundary as its own gate.

5. Data isolation

Sandbox rows live in your tenant schema, marked with the test stamp on the message or provider: "sandbox" on the number. They are never metered or billed — cost_usd_cents: 0, no wallet debit, no Stripe traffic — and they are excluded from metrics by filtering on metadata.test_mode. The tenant-isolation model itself lives in Tenant isolation. Purge semantics: POST /sandbox/reset wipes sandbox contacts, conversations, and messages and releases sandbox numbers, so a test run can start from a clean slate. The route rejects any non-sandbox request with 403 SANDBOX_ONLY before a single row changes, so a live context can never wipe production data through it.

6. Transition to production

At promotion, configuration carries over; traffic does not. Mint a live key, point the same integration code at it, and the following survive: webhook subscriptions and URLs, routing configurations, flows, and reusable templates or lists. The following do not: sandbox message rows, status histories, and purchased sandbox numbers — those stay test-scoped and are wiped by reset. Re-purchase live numbers and re-register a live sender against the live credential; the pre-launch checklist reports readiness while any of those steps is pending. Carries over vs. does not:

7. Pointers