Skip to main content
A sandbox key lets you build and test the send path — validation, eligibility, webhooks, signature verification — without delivering a message or incurring a charge. Sends run the live gates that do not create durable link state, then short-circuit to a simulated result.

Getting a sandbox key

Create a sandbox key from the dashboard under Settings → API Keys with Sandbox mode enabled. A key whose value starts with trk_test_ is also always treated as a sandbox key. Dashboard-created sandbox keys use the ordinary trk_ prefix, so a client cannot tell a sandbox key from a live key by looking at it: call GET /v2/auth/whoami and read api_key.sandbox.
The partner key-mint endpoint issues live keys only. Provision sandbox keys from the dashboard.
Use a sandbox key exactly like a live key — the same endpoints, the same headers. The response tells you it’s simulated:
The send is not recorded in your message history, nothing is queued to the carrier, and no usage is billed.
Sandbox keys are read-only for the account hierarchy — creating children, minting keys, and suspending all require a live parent key (403 sandbox_read_only).

Deterministic outcomes

Send to a reserved recipient number to force a specific outcome. Any other recipient simulates delivered. A failed outcome also returns error_code: "SANDBOX_CARRIER_REJECT" and an error_description, mirroring a real carrier rejection.

Simulated signed webhooks

A single sandbox send fires the matching signed webhook to your account’s configured endpoints — a real HTTP POST with a real X-Trackly-Signature, with data.sandbox: true on the envelope. This is the end-to-end way to prove your signature verification works before going live: send to +15005550101, receive a message.failed event, and confirm your verifier accepts it.
Bulk sends do not fire webhooks. A bulk simulation would fan out one POST per message; to test webhook handling, use single sends to the reserved numbers.
Webhook dispatch is best-effort — if your endpoint is down, the simulated send still returns 201.

Live validation parity

A sandbox send runs the same side-effect-free checks first and returns the same error codes on failure:
  • Payload shape and E.164 validation (missing_to, invalid_phone, …)
  • Sending-list ownership (list_not_found)
  • BYOC webhook verification (webhook_not_configured)
  • Double opt-in (pending_confirmation, doi_expired)
  • Suppression / opt-out (contact_suppressed)
  • Free-tier BYOC requirement (free_tier_non_byoc)
  • linkOverrides shape, strict wrap_links boolean, supported arm, public HTTPS URL policy, 2,048-character URL limit before and after macro expansion, Partnerships entitlement, and active account-owned offer eligibility (invalid_wrap_links, link_override_invalid, link_override_unsafe_url, link_overrides_require_wrap_links, offer_not_found, partnership_offer_not_found, partnerships_not_enabled). With a non-empty linkOverrides object, wrap_links must be a JSON boolean and must be true; requests without effective overrides retain legacy coercion behavior.
For a single sandbox send, those failures use the same 400, 403, or 404 status as a live send. In a sandbox bulk request, partnerships_not_enabled rejects the whole request with 403; the 400 parser/URL errors and 404 offer errors appear per message in errors[], and the request returns 201.
Two side-effecting behaviors are not simulated: warm-up and link minting. linkOverrides are validation and preload only: no short link, self-affiliate, or offer-access state is created, and {{linkN}} stays unsubstituted in the body used for segment measurement. Sandbox returns no_domain_configured when a referenced override has no usable shortener and offer_access_not_active when existing access is paused or rejected; it does not create missing access. A live wrap_links=true send may report a different segment count than its sandbox preview.

Preflight

Check eligibility, segments, and cost without sending — works with live keys too.

Webhook signing

Verify the signature on the webhooks your sandbox sends fire.