API reference

Base URL, request and response conventions, and the shape of every Mapier /v1 endpoint.

The Mapier API is a small HTTP surface over iMessage. You queue commands, you read the results back on a single server-sent events stream or as signed webhooks, and everything you can reach with a key is described in one OpenAPI document — every example in these docs is a plain fetch or curl you can paste into a terminal, and any generator will turn the document into a typed client.

Base URL

https://api.mapier.ai

Every path is prefixed with /v1. The major version is part of the path, and a future /v2 would sit alongside /v1 rather than replacing it in place. Within /v1, the changes a deployed integration could trip over — a request resolved differently, or a word it switches on renamed — ship behind a dated version you choose with the x-mapier-version header, which every route reads — see Versions.

The OpenAPI document

https://api.mapier.ai/openapi.json

It describes /v1 and only /v1 — every field with a sentence, every error code each operation can answer with and the status it answers with it.

Operation
POST /v1/commands/{type}Send a command; the 202 is a handle.
GET /v1/response_streamSettlements, inbound messages and, on request, receipts (SSE).
GET /v1/whoamiWhat this key is, what it can send from, and its command ceiling.
POST /v1/handle-checkIs this address reachable.
GET /v1/attachments/{id}Fetch an attachment.
GET /v1/messages/{messageId}/receiptsAsk what one message's receipts say, now.
GET /v1/conversations/{conversationId}/messagesRead a conversation's history, newest first.
GET /v1/lines/availableWhich of your numbers somebody new would get now.

The document describes these eight and nothing else.

The shape of every request

curl https://api.mapier.ai/v1/commands/message.send \
  -H "Authorization: Bearer $MAPIER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "target": { "kind": "recipient", "channel": "imessage", "recipientExternalId": "+14155550100" },
    "payload": { "text": "hello" }
  }'

Three things are always true of a command request:

  • It is a POST whose body, when it has one, is a JSON object. A body that is not JSON at all is 400 bad_request; JSON that is not an object — an array, a string, a number — is 422 validation_failed.
  • The body has two top-level keys: target (who) and payload (what). Both are objects; a command that needs neither may omit the body. See Addressing a conversation.
  • The body is at most 65,536 bytes. Larger is rejected with 413 payload_too_large before any parsing happens — and before the key is checked, because reading a body it is going to refuse is the one thing an unauthenticated caller can make the service do.

See Authentication for how requests are identified.

The shape of every response

Successful command submissions return 202 Accepted:

{
  "commandId": "3f1a0c2e-8b47-4d19-9a5e-2c6f0b8d4e71",
  "status": "queued",
  "disposition": "queued",
  "lineId": "0192a7b0-3e4f-7051-8c6d-7e8f9a0b1c2d",
  "lineSelection": "reused_active_chat"
}

Prop

Type

The 202 is always queued, and the command frame is the one that settles: succeeded, failed, cancelled or expired, or ambiguous or no_effect when the transport took the command and could not prove what it changed.

Every refusal is one envelope:

{ "error": { "code": "not_found", "message": "That conversation does not exist." } }

Switch on code: it is a closed set and each value maps to exactly one HTTP status. message is a sentence for a person and may change; never parse it. details, when present, is an array of { path, message, … } entries on a 422 validation_failed — one per field that failed — and an object on the other refusals that carry one: a reason to switch on, such as a 503 line_unavailable's, and the fields that go with it. A 429 from a route's own quota also carries a Retry-After header. See Error codes for the complete list.

202 does not mean it worked

This is the single most important thing to understand about this API.

A 202 means the command was durably queued, not that it was delivered. The command is dispatched to the line, executed against iMessage, and settles asynchronously — success or failure arrives on the response stream as a command event, typically within a few seconds.

Not every payload is checked before the 202. A message.send payload is checked before anything is written — a key the line cannot carry is a 422 validation_failed at payload.<key>, never dropped. A reaction.tapback, name_photo.share or group.create payload can be accepted with 202 and then settle failed on the stream when the transport refuses it — see what is checked before anything is queued. If you are testing a new payload shape, watch the stream.

Endpoints

MethodPathPurpose
POST/v1/commands/:typeQueue a command
GET/v1/response_streamSettlements, inbound messages and, on request, receipts (SSE)
GET/v1/whoamiWhat this key is, its lines, and its command ceiling
POST/v1/handle-checkTest whether an address is reachable
GET/v1/attachments/:idDownload an inbound attachment
GET/v1/messages/:messageId/receiptsAsk what one message's receipts say, now
GET/v1/conversations/:conversationId/messagesRead a conversation's history, newest first
GET/v1/lines/availableWhich of your numbers somebody new would get now

Every one of them reads x-mapier-version.

Conventions

Identifiers. Phone numbers are E.164 (+14155550100). Email addresses are lowercase and NFC-normalised. Conversation ids are Mapier UUIDs, not iMessage GUIDs — you get them from inbound message events.

Timestamps. ISO 8601 with a Z suffix. occurredAt on an inbound message is when iMessage says it was sent, not when Mapier received it.

Forward compatibility. Fields are added to responses and events without a version bump. Parse defensively: ignore keys you do not recognise, and treat enum values you do not recognise — especially errorCode — as unknown rather than impossible. A change your integration could not absorb that way — a request resolved differently, a word renamed — waits behind a dated version instead.

Next

On this page