Send your first message
Get access, create a project and a test key, open the response stream, send to a sandbox contact, and watch the outcome arrive.
There is nothing to install. The Mapier API is plain HTTP with a bearer key,
so curl and fetch are the whole toolchain — and every request and response
shape is in the OpenAPI document at https://api.mapier.ai/openapi.json, which
any generator will turn into a typed client in the language you already use.
What takes five minutes is not the request — it is understanding that the
request only tells you half the story. A send returns 202 immediately, and
the actual outcome arrives a moment later on a separate connection. So you open
that connection first.
Get access, then create a project and a test key
Access to Mapier is by approval. Registration is closed: anybody can sign in, but creating an organization needs an address Mapier has approved — for a test project and its sandbox as much as for a live line. Book a demo and we will set your account up on the call. Invited to an organization that already exists? The invitation is the approval: sign in with the address it was sent to.
Once your address is approved, sign in at app.mapier.ai, create an organization, and create a project. A project is born with a sandbox: a fake iMessage world with a line of its own and three contacts — Riley Chen, Sam Okafor and Jordan Vale — each with a handle in a number range no carrier issues, so nothing you send in this guide can reach a person.
Open the project's Keys page and create a test key. The plaintext is shown once; copy it now.
export MAPIER_API_KEY="mp_test_…"Then open the Sandbox page and copy Riley's handle from the roster (the
settings gear beside the name). It begins +198.
export RILEY="+198…"Authentication covers what the key identifies and how to keep it out of your logs; Test mode covers exactly where the sandbox differs from a handset.
Check what the key is
curl https://api.mapier.ai/v1/whoami \
-H "Authorization: Bearer $MAPIER_API_KEY"{ "projectId": "…", "environment": "test", "keyId": "…", "lines": [ { "address": "+199…", "channel": "imessage", … } ], "limits": { "commandsPerMinute": 600 } }environment answers "did I paste the live key or the test one?" without
sending anything to anybody. lines is what this key can send from — in test,
the sandbox's own line. limits.commandsPerMinute is how many commands your
project may send a minute in this environment.
Open the response stream first
Open a second terminal and start the stream before you send anything. It is a
long-lived Server-Sent Events connection, and -N stops curl buffering it.
curl -N https://api.mapier.ai/v1/response_stream \
-H "Authorization: Bearer $MAPIER_API_KEY" \
-H "Accept: text/event-stream"Within a moment you get the first bytes — a retry: hint, then a comment —
and then a heartbeat every 25 seconds:
retry: 3000
: connected
: pingBoth lines begin with a colon, which makes them SSE comments rather than events. They tell you the socket is alive and nothing more.
This ordering is the whole point of the page. A 202 from a send means the
command was queued — not that a message was delivered. The stream is where
the outcome appears. Every frame on it carries an id:; after a drop,
reconnect with Last-Event-ID set to the last one you handled and the stream
replays what you missed.
Send a message
Back in the first terminal, send plain text to Riley. The verb goes in the
path, and the body is two keys: target (who) and payload (what).
curl -X POST https://api.mapier.ai/v1/commands/message.send \
-H "Authorization: Bearer $MAPIER_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: first-hello" \
-d "{
\"target\": { \"kind\": \"recipient\", \"channel\": \"imessage\", \"recipientExternalId\": \"$RILEY\" },
\"payload\": { \"text\": \"Hello from the Mapier API.\" }
}"The response comes back straight away:
{ "commandId": "…", "status": "queued", "disposition": "queued" }Keep commandId. It is how you recognise this command's outcome on the stream.
status is always queued on a 202 — the terminal status is the stream's to
report.
Repeat the exact request and you get the same commandId back with
"disposition": "adopted" instead of a second message: a key the platform has
already seen adopts the command it named. Leave the header off and every
attempt is a new send — the platform mints a key for each request, so two
requests with no header are two messages.
Watch it settle
Switch to the stream terminal. Within a second or two a command frame
arrives — the sandbox settles in milliseconds, where a real handset takes
seconds:
id: 0192…
event: command
data: {"commandId":"…","logicalActionId":"first-hello","status":"succeeded","errorCode":null,"conversationId":"…","failureClass":null}Only terminal statuses are emitted, once per command:
succeeded | failed | ambiguous | no_effect | cancelled | expired. ambiguous
and no_effect mean the transport took the command and could not prove what
it changed — unknown, not failed. Had it failed, the frame would carry the
transport's errorCode and a failureClass beside it: the code's set is open
and the class's is closed, so branch on status, then on failureClass.
conversationId names the thread the message went into; send the next one
there.
Then open the project's Sandbox page. Riley's phone has your message on it, and the stamp under the bubble reads Simulated · Delivered — the sandbox's honest word for an outcome it decided rather than observed.
Succeeded is not delivered
For message.send, succeeded is the transport's settlement of the send, not a delivery receipt.
Delivered and read arrive as receipt frames, and a provider's "not delivered" as a
delivery_failed frame, on a stream opened with ?events=receipts,delivery_failures — see
Receipts and delivery verdicts. Neither changes the command's
status.
Get a reply
On the Sandbox page, pick Riley on the roster and type something on the phone — you are composing as Riley. The reply arrives on your stream as the second kind of frame:
id: 0192a7c4-…
event: message
data: {"conversationId":"a0000000-…","from":"+198…","text":"got it","externalId":"…","replyToExternalId":null,"occurredAt":"2026-09-14T09:24:31.000Z","attachmentIds":[],"attachments":[],"messageId":"0192a7c4-…"}Set Riley's auto-reply (the gear beside the name) to Echoes what it is sent and every message you send comes straight back, which is enough to drive an integration test end to end without a person at the other handset.
Going live
A live key takes exactly the same routes; only the transport behind them
differs. Request a live line from the project's Lines page — a live line is
a real Apple account on a real Mac, provisioned by Mapier's operations team
rather than by an instant API call — then create a mp_live_ key and swap the
environment variable. Test mode lists the handful of
places the sandbox behaves differently from a handset, so a rehearsal that
passed does not surprise you on the day.
What to know before you scale
- The stream resumes. Every frame carries an
id:; keep the last one you handled somewhere durable and reconnect with it asLast-Event-ID. The replay is at most 500 frames per batch and reaches back 90 days. Reconnect in a loop with backoff — the connect budget is 10 per minute per key. See Reconnecting. - Webhooks are the other half. The same events, POSTed to your endpoint, signed, retried and replayable from the console. See Webhooks.
- Send an
Idempotency-Key, and the same one on every retry. A repeated key is adopted on every command in both environments exceptgroup.create,typing.startandmessage.readon a live line, which ignore it. A request with no key gets one minted for it alone, so its retry is a second command. See Idempotency. - Commands are budgeted per project, the rest per key, each over a
one-minute window: 600 commands per project and environment by default
(
GET /v1/whoamireports yours), and per key 30 handle checks, 120 attachment downloads, 120 history reads and 10 stream connects. A429carries aRetry-Afterheader. See Rate limits.