Test mode
What a test key reaches, and the four places the sandbox behaves differently from a handset.
Every project has two environments, and a key names one of them: mp_live_…
reaches real lines and real handsets, mp_test_… reaches the project's
sandbox and nothing else. Both take exactly the same routes — the whole of
/v1 — and only the transport behind them differs, which is the point of
having a test mode at all. The two see none of each other's data: a
conversation, a contact, an event or a webhook endpoint belongs to one
environment, and a test key asking about a live id gets 404.
The sandbox
A sandbox is a fake iMessage world: one line of its own (+199…), three
contacts — Riley Chen, Sam Okafor and Jordan Vale, at handles in a number range
no carrier issues — and a phone you hold on the project's Sandbox page,
which is the contact's device rather than yours. Compose there and you are
composing as Riley; what your agent sends lands on Riley's screen.
The device is real in the one way that matters: a send is carried out by the
same executor a live relay uses, against a simulated gateway, so a refusal in
test is the refusal live gives. Ask to send to a handle the world does not know
and the command settles failed / send_returned_not_ok, exactly as an address
no device answers to would settle on a Mac.
Each contact carries settings that decide what happens to a send:
| Setting | What it does |
|---|---|
| Delivery mode — Delivers · Delivers, then reads · Fails to deliver · Never confirms · Unreachable | The outcome a send to this contact gets. Never confirms is the one a real handset produces only by accident: the send leaves and settles needs_reconcile with gateway_response_lost, which is the state your handler most needs to have seen once. |
| Auto-reply — Stays quiet · Echoes what it is sent | Whether the contact answers. Echo is enough to drive an integration test end to end with nobody at the other handset. |
| Delivery delay (world-wide) | How long a send waits before the world settles it, 0–60 s. |
Every stamp the sandbox produces is labelled Simulated, and every
message.delivered / message.read event it emits carries
"simulated": true — the sandbox never lets a rehearsal masquerade as a
receipt.
Where it differs from a handset
Four divergences, and each is worth knowing before a rehearsal that passed surprises you on the day. The Sandbox page prints the same four under the phone.
- Settlement is immediate. A test send settles in milliseconds. There is
no
acceptedstep on the way and no retry ladder underneath it — what the relay spends seconds negotiating, the world decides at once. A client that only works because a live send is slow will find out here. - Delivery timing is a knob, not a measurement. The delay is a number the world waits before settling. It is not modelled on any handset and it is the same for every contact.
- Nothing here asks Apple. Every handle in the world exists because the
world made it.
POST /v1/handle-checknever leaves the database, so it can neither find a real iMessage account nor miss one — it answersavailable: truefor the world's own contacts andavailable: falsewithreason: "not_in_sandbox"for everyone else. - The device behaves as you tell it to. Delivered, read, failed, unconfirmed and unreachable are settings on a contact. That is the point — those are the outcomes a real handset only produces by accident — but a green stamp here is your own choice reflected back, not a fact about a phone.
Two more that are about surface rather than timing:
- Commands the sandbox can execute:
message.send,reaction.apply,reaction.tapbackandname_photo.share. The group commands (group.create,group.update,group.leave) are accepted and settlefailed / unsupported_capability— which is deliberately not the404 unsupported_commandan unknown type gets, because "this environment cannot do it" is worth retrying against a live key and "the platform has never heard of this" is not. No attachments, no inbound media, no typing. - Validation happens at the POST. The sandbox validates a payload and
answers
422 validation_failedsynchronously; a live line defers that to the Mac and reports it as afailedsettlement on the stream. Handle both.
Contacts in test
A test contact is only ever created by the sandbox — on the Sandbox page, or
by the world when a project is born. POST to the console's contacts route in
test is a 409 pointing at the Sandbox page, and a /v1 send to an address
the world does not know fails the way it fails live. A test that quietly
provisioned a contact on first send would make test mode stop resembling the
thing it is a test of.
Resetting the world
Reset world on the Sandbox page starts a new generation. Nothing is
deleted: every message, command and event stays on the record, the phone
empties, the roster's counters reset, and — the part a test harness cares
about — every Idempotency-Key used in the previous generation works again,
so a fixture can send first-hello on every run. The ids the world handed out
are never reused.
Budget
Test traffic is budgeted rather than metered: nothing in test is billed, and
each world admits a bounded number of actions per UTC day (the Sandbox page's
Budget meter, N of M actions today). Past it, a command answers 429 sandbox_budget_exhausted until the day rolls over.