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"
200 OK
{ "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:

Stream output
retry: 3000
: connected

: ping

Both 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:

202 Accepted
{ "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:

Stream output
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:

Stream output
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 as Last-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 except group.create, typing.start and message.read on 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/whoami reports yours), and per key 30 handle checks, 120 attachment downloads, 120 history reads and 10 stream connects. A 429 carries a Retry-After header. See Rate limits.

On this page