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:

SettingWhat it does
Delivery mode — Delivers · Delivers, then reads · Fails to deliver · Never confirms · UnreachableThe 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 sentWhether 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.

  1. Settlement is immediate. A test send settles in milliseconds. There is no accepted step 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.
  2. 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.
  3. Nothing here asks Apple. Every handle in the world exists because the world made it. POST /v1/handle-check never leaves the database, so it can neither find a real iMessage account nor miss one — it answers available: true for the world's own contacts and available: false with reason: "not_in_sandbox" for everyone else.
  4. 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.tapback and name_photo.share. The group commands (group.create, group.update, group.leave) are accepted and settle failed / unsupported_capability — which is deliberately not the 404 unsupported_command an 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_failed synchronously; a live line defers that to the Mac and reports it as a failed settlement 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.

On this page