mapier docs

Send your first message

Open the response stream, send a message over HTTP, and watch the outcome arrive.

Preview API — the base URL below is a development host, and the production hostname is not assigned yet.

There is nothing to install. The Mapier API is plain HTTP with a bearer credential, so curl and fetch are the whole toolchain, and there is no client library to keep in step with the service.

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 seconds later on a separate connection. So you open that connection first.

Get a credential

Access is not self-serve today: contact Mapier to have an account and a Mac connector provisioned for you. You will be given a single key.

Export it as MAPIER_API_KEY — every example below reads it from the environment. Authentication covers what the credential identifies and how to keep it out of your logs.

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 $MAPIER_BASE_URL/v1/response_stream \
  -H "Authorization: Bearer $MAPIER_API_KEY" \
  -H "Accept: text/event-stream"

Within a moment you get the first bytes, and then a heartbeat every 25 seconds:

Stream output
: 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 written down for your Mac to pick up — not that a message was delivered. The stream is the only place the real outcome appears: there are no webhooks, and no endpoint to poll a command's status after the fact. It also has no replay or backfill, so anything emitted while you were not connected is gone for good.

If this returns 404 no_connector, your account has no Mac attached yet and nothing you send will ever settle. That is a provisioning problem, not a code one.

Send a message

Back in the first terminal, send plain text to a handle you control — your own phone is the easiest target. The verb goes in the path, and the body is exactly two keys: target (who) and payload (what).

curl -X POST $MAPIER_BASE_URL/v1/commands/message.send \
  -H "Authorization: Bearer $MAPIER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "target": { "kind": "recipient", "recipientExternalId": "+14155550100" },
    "payload": { "text": "Hello from the Mapier API." }
  }'

The response comes back straight away:

202 Accepted
{
  "commandId": "cmd-1",
  "status": "queued",
  "disposition": "queued"
}

Keep commandId. It is how you recognise this command's outcome on the stream.

A recipient target addresses a person by handle and starts a new thread. It is deliberately limited: plain text only, and not retry-safe — sending the same request twice sends two messages. Both limits disappear once you have a conversation id, which is step 5.

Watch it settle

Switch to the stream terminal. A few seconds after the 202, the settlement appears:

Stream output
event: command
data: {"commandId":"cmd-1","logicalActionId":"cmd-1","status":"succeeded","errorCode":null}

There is exactly one of these per command, and only terminal statuses are ever emitted — the internal claimed, executing and accepted steps are suppressed, so you will never see a command progress. It queues, then some time later it lands on one of succeeded, failed, no_effect, ambiguous, cancelled or expired.

Had it failed instead, the same event would differ only in its last two fields:

Stream output
event: command
data: {"commandId":"cmd-1","logicalActionId":"cmd-1","status":"failed","errorCode":"invalid_target"}

errorCode is a string when something went wrong and null otherwise. The set of values is open and will grow, so branch on status first and treat an errorCode you do not recognise as a generic failure rather than an unreachable case.

Succeeded is not delivered

succeeded means iMessage accepted the send. It is not a delivery receipt — nothing in the API tells you the message rendered on the other device, and there are no read receipts or typing events either.

Reply into the conversation

When the recipient replies, a second kind of event arrives on the same stream:

Stream output
event: message
data: {"conversationId":"a0000000-0000-4000-8000-000000000001","from":"+14155550100","text":"got it","externalId":"guid-1","replyToExternalId":null,"occurredAt":"2026-08-29T09:24:31.000Z","attachmentIds":[]}

That conversationId is the thing you were waiting for. It is a Mapier UUID naming the thread — not the iMessage GUID, which is the separate externalId field on each message. You cannot construct a conversation id and there is no endpoint that looks one up, so an inbound message is how you get one. (The settlement of a successful group.create can also carry the id of the group it created, but that is the only other source.)

Store it against your own records, and address the thread with a conversation target from now on. That switches the command onto a different routing path, which is where rich content and retry safety live:

curl -X POST $MAPIER_BASE_URL/v1/commands/message.send \
  -H "Authorization: Bearer $MAPIER_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: welcome-a0000000-0000-4000-8000-000000000001" \
  -d '{
    "target": {
      "kind": "conversation",
      "conversationId": "a0000000-0000-4000-8000-000000000001"
    },
    "payload": {
      "kind": "rich_text",
      "text": "Thanks — this one can carry an effect.",
      "effect": "confetti"
    }
  }'

Two things changed. The payload gained a kind, unlocking effects, replies, links and polls. And the Idempotency-Key header is now honoured: repeat that exact request and you get the same commandId back with "disposition": "adopted" instead of a second message.

Note that your own send does not echo back on the stream. Only inbound messages produce a message event.

What to know before you scale

The four things most likely to bite you between a working curl and a working integration:

  • The stream is at-most-once, with no resume. No event ids, no Last-Event-ID, no cursor, no backfill, and no ordering guarantee. The socket drops periodically even when everything is healthy, so reconnect unconditionally in a loop and accept the gap. Opening several streams does not help: every connection receives every event, so it is a broadcast, not a work queue.
  • Idempotency only covers some commands. Conversation sends, tapbacks, reaction.apply and contact-card sharing honour Idempotency-Key. Recipient sends and all group commands silently ignore it and mint a fresh key per call, so a retry there really does send twice. See Idempotency.
  • Payloads are not validated at the API boundary. A 202 confirms the envelope was well-formed and nothing else. A malformed payload is accepted, then settles failed on the stream — which is another reason to be reading it before you send.
  • Rate limits are per 60-second rolling window: 60 commands, 30 stream connections, 120 attachment downloads, 30 handle checks. A 429 carries both a Retry-After header and a retryAfter field. Note the stream limit counts connections, so a reconnect loop that retries without backoff will exhaust it.

On this page