Sandbox & test numbers

Test keys (ssms_sk_test_) run against a simulated environment: no charges, and no text is sent to any phone, with one exception, your own verified phone (below). Every endpoint works, so you can build your whole integration, including webhooks, before going live.

Simulated or real

A simulated message looks exactly like a real one. Its status goes to sent and then delivered, it has a destination_carrier (Sandbox Wireless), and the same events and webhooks fire. None of that means a handset received anything. Three things tell you which it was:

WhereSimulatedReal
simulated on the message, and on message.sent, message.delivered, message.failed, message.received, message.opted_out, message.opted_in and message.help_requested event payloadstruefalse
X-TextKit-Simulated header on the response to POST /v1/messages (test keys)truefalse
notice on the response to POST /v1/messagesPresent when to is not a +1 500 555 test number, e.g. "Simulated: nothing was sent to +14155550132. Test keys only deliver to your verified phone. Verify it at https://jointextkit.com/console/settings or use a live key."Absent

Every live message has "simulated": false. With a test key, only a send to your own verified phone can be real. A test-key send to any other real number, including one of your verified test recipients, is simulated: nothing arrives. The console shows the same thing. It lists live and test messages together; a test message is tagged Test, a simulated one Test · simulated, and it says "Nothing was sent to a phone" when you open it. API Keys → Test credentials lists your sandbox number and every magic number below.

Your sandbox number

Signup provisions a sandbox number in the reserved +1 500-555-XXXX range. It's the from for outbound tests and the to for simulated inbound. You can "purchase" more from GET /v1/numbers/available + POST /v1/numbers; the whole numbers API works in the sandbox.

Your own phone

Sign-up offers to verify your cell as its last step (optional, and you can do it later under Settings): we text you a code. Once it's verified, a test key delivers to that one number for real, so your first API call makes your phone buzz. Every other destination stays simulated. Real sandbox texts are capped at 10 a day and end with - via TextKit sandbox. The response is a normal message with "test": true, "simulated": false and an X-Sandbox-Real-Delivery: true header. Your phone also counts as your first verified recipient when you go live on the free tier.

Real delivery to your phone needs a verification sender on our side. If none is available the send is simulated like any other, and says so: "simulated": true with a notice that real delivery is unavailable right now. Nothing is ever reported as real that was not handed to a carrier.

Magic destination numbers

Send to these numbers to trigger fixed behaviors:

NumberBehavior
+15005550006Delivered: message.sent then message.delivered; the message is delivered by the time the send returns. Any other number behaves the same.
+15005550013Delayed delivery: status sent, then really delivered at least 5 seconds later. Poll GET /v1/messages/{id} (it settles as soon as the delay has passed) or wait for the message.delivered webhook (within about a minute). Use it to test code that waits on delivery.
+15005550001Stuck: status stays queued forever, no delivery event.
+15005550002Failed, no reason given: failure.code unknown.
+15005550007Failed: filtered as spam, failure.code carrier_filtered.
+15005550008Failed: refused by the carrier, failure.code carrier_rejected.
+15005550009Failed: number not in service, failure.code invalid_number.
+15005550010Failed: a landline, failure.code landline (and GET /v1/lookup says landline).
+15005550014Failed: phone unreachable, failure.code unreachable.
+15005550011Opted out: 403 recipient_opted_out, exactly as for a recipient who replied STOP. Nothing is written to your opt-out list; the refusal is stored as a blocked message.
+15005550012Rate limited: 429 rate_limited with a Retry-After header. The refusal is stored as a blocked message.
+15005550016No opt-in on file: a marketing message ("category": "marketing", or any broadcast or batch) answers 403 consent_required. The other 500-555 numbers count as opted in, so test broadcasts to them deliver. See opt-in on file.
+15005550021 to +15005550027Verify only: a code to one of these answers 403 verification_blocked with one Verify Shield reason each (destination_cooldown, destination_velocity, tenant_velocity, ip_velocity, voip_not_supported, unsupported_region, toll_free_destination), like a live key. See Verify.
+15005550015Quiet hours: a recipient for whom it is always night. A marketing message answers 403 quiet_hours, or 202 with a held scheduled message when you add "quiet_hours": "defer". Any other message answers 202 with a held scheduled message. Only a message that reads as a one-time code delivers. See quiet hours.

Failed numbers return 201 with status failed and emit message.sent then message.failed; the failure object and the event payload are the same ones a real carrier failure produces (see Errors). Every sandbox message has a full timeline, segments, encoding, a price of 0, and destination_carrier from the sandbox lookup.

Simulated inbound

POST /v1/test/inbound delivers a fake inbound SMS to one of your sandbox numbers through the real pipeline: it appears in GET /v1/messages and emits a message.received event with "simulated": true. (Test keys only; live keys get a 403 test_mode_only.) to must be a sandbox number.

STOP, START, YES and HELP behave as they would for real, so you can rehearse opt-out. Once your account has live access its opt-out list is real, so a simulated message changes it only when from is a +1 500 555 number or one of your verified numbers. From any other number the message is handled as an ordinary one, consent unchanged, and the response carries a notice saying so.

Test and live data

Once your account has live access, each key sees only its own mode's data, as in Stripe. A test key lists and reads sandbox messages, events, batches, schedules, scheduled messages, automations, numbers, exports and webhook endpoints; a live key the live ones. Asking for an item of the other mode answers 404 not_found, with a message saying which key to use. Every event says which it is ("mode": "test" or "live"), and a webhook endpoint receives only its own mode's events. Before live access there is no live key, so your test keys see everything on the account, as they always have.

A test key never changes anything live: it can't send or schedule from a live number, or change a live schedule, automation, batch, scheduled message or number (403 forbidden), and a bulk number action reports mode_mismatch for each number of the other mode.

Some data belongs to the whole account: contacts, segments and topics, the consent list, templates, customers, registrations, number pools, the spend limit and the audit log. On a sandbox account test keys use it as before. Once your account has live access, it needs a live key (403 live_access_required). Balance and usage stay readable with either key.

The key textkit login makes is a test key restricted to what the CLI does: send and read messages, Verify, numbers, lookups and events.

Sandbox limits

A ceiling of 1,000 messages/day per account keeps the sandbox healthy. It's not a product quota. If you legitimately hit it, tell us.