# Sandbox & test numbers

Source: https://jointextkit.com/docs/sandbox
Index: https://jointextkit.com/llms.txt

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:

| Where | Simulated | Real |
| --- | --- | --- |
| `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 payloads | `true` | `false` |
| `X-TextKit-Simulated` header on the response to `POST /v1/messages` (test keys) | `true` | `false` |
| `notice` on the response to `POST /v1/messages` | Present 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](/console/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:

| Number | Behavior |
| --- | --- |
| `+15005550006` | Delivered: `message.sent` then `message.delivered`; the message is `delivered` by the time the send returns. Any other number behaves the same. |
| `+15005550013` | Delayed 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. |
| `+15005550001` | Stuck: status stays `queued` forever, no delivery event. |
| `+15005550002` | Failed, no reason given: `failure.code` `unknown`. |
| `+15005550007` | Failed: filtered as spam, `failure.code` `carrier_filtered`. |
| `+15005550008` | Failed: refused by the carrier, `failure.code` `carrier_rejected`. |
| `+15005550009` | Failed: number not in service, `failure.code` `invalid_number`. |
| `+15005550010` | Failed: a landline, `failure.code` `landline` (and `GET /v1/lookup` says `landline`). |
| `+15005550014` | Failed: phone unreachable, `failure.code` `unreachable`. |
| `+15005550011` | Opted 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. |
| `+15005550012` | Rate limited: `429 rate_limited` with a `Retry-After` header. The refusal is stored as a `blocked` message. |
| `+15005550016` | No 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](/docs/compliance-checks#opt-in-on-file). |
| `+15005550021` to `+15005550027` | Verify 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](/docs/verify#shield). |
| `+15005550015` | Quiet 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](/docs/compliance-checks#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](/docs/errors#delivery-failures)).
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](/docs/webhooks#test-and-live-endpoints) 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.
