Using these docs with AI agents
Machine-readable copies of every page, and the two things an agent writing against this API most often gets wrong.
If you are pointing a coding agent or an LLM at this API, you do not have to scrape the HTML. Every page here is also published as plain markdown at a stable URL.
The machine-readable views
| Path or header | What it is |
|---|---|
/llms.txt | An index of every page, with titles and descriptions |
/llms-full.txt | The entire documentation set as one markdown document |
<page path>.md | The markdown source of a single page |
Accept: text/markdown | The same markdown, at the page's own canonical URL |
/llms.mdx/docs/<path>/content.md | The same markdown again, at a separate enumerated path |
The last three are three spellings of one document, and the first of them is
the one to reach for: take any page path and append .md.
curl https://docs.mapier.ai/api/message-send.mdContent negotiation gets the same body without changing the URL, which suits a
tool that already holds the canonical link and only wants a different
representation of it. It also covers the index page at /, which has no path
segment to append to.
curl -H 'Accept: text/markdown' https://docs.mapier.ai/api/message-sendThe long /llms.mdx/docs/<path>/content.md form is the same page again —
/api/message-send is published at /llms.mdx/docs/api/message-send/content.md.
It is listed here because it is stable and enumerable, not because anything that
can build one of the two forms above needs it.
All of them are plain text over HTTP. There is no authentication, no key and no rate limit to negotiate — they are the same public pages in a different representation.
Which one to reach for
/llms.txt for navigation. It is small, so it fits comfortably in a system
prompt or a first tool call. An agent that reads the index can then fetch only
the two or three pages a task actually needs, which keeps the context focused
and the answers grounded in the right page.
/llms-full.txt for a complete dump. Use it when you want the whole API in
one context window and can afford the tokens — a one-shot integration, a
migration review, a question that could touch anything. It is the same content
as the site, concatenated into a single document.
A single page for a targeted fetch. When you already know the subject —
error codes, the stream contract, the group commands — fetch that page alone,
as <path>.md. It is by far the cheapest option and the least likely to bury
the relevant paragraph.
A reasonable default is: load /llms.txt once, then fetch pages on demand. The
index gives site-root paths (/api/errors), so the fetch is that path plus
.md.
Prompting against this API
Two facts about this API contradict what a model has learned from every other messaging API it has seen, and both produce code that looks correct and is quietly broken. State them explicitly in your prompt rather than hoping the docs get read closely enough.
A 202 is not a delivery confirmation. It means the command was queued.
Agents routinely write a send() that returns as soon as the HTTP call
succeeds, and then a caller that reports success. The real outcome arrives
later as a command frame on GET /v1/response_stream. For message.send,
succeeded is the transport's settlement, not a delivery receipt: delivered and
read are receipt frames and a provider's "not delivered" is a
delivery_failed frame, both only on a stream opened with
?events=receipts,delivery_failures, and neither changes the command's status.
Work the transport could not prove settles ambiguous or no_effect. A
message.send payload the line cannot carry is a 422 at payload.<key>
before a command exists. So are:
- a malformed
reaction.tapbackorname_photo.shareon a live line; - on an iMessage line in either environment, a tapback, share,
group.create,group.update,group.leave,message.edit,message.unsend,name_photo.get,name_photo.set,chat_background.setorchat_background.removewhose conversation, message, participants, line or payload cannot be addressed.
Any other malformed payload can be accepted with 202 and fail later on the
stream.
Idempotency-Key does not work on every command, and leaving it off is not
safe. A repeated key is adopted on every command but group.create,
typing.start and message.read on a live line, which accept the header and ignore it. A request with no key gets
one minted for it alone, so its retry is a new command. An agent writing a
generic retry wrapper will assume the header is universal — or optional — and
produce a client that duplicates groups and messages under load, or reuse a key
after its command has settled, which adopts the old command and sends nothing
at all.
Something like this, pasted alongside your task, is usually enough:
The Mapier iMessage API returns 202 when a command is QUEUED, not delivered.
Never report success from the HTTP response. Subscribe to GET /v1/response_stream
and wait for the `command` event carrying that commandId.
For message.send, succeeded is the transport's settlement of the send, not a
delivery receipt. Do not mark a message delivered on succeeded. Delivery and read
arrive as `receipt` frames, and a provider's "not delivered" as a
`delivery_failed` frame, only on a stream opened with
?events=receipts,delivery_failures. Neither changes the command's status, and a
delivery_failed frame can arrive before the send's own command frame, so
correlate every frame by commandId. A command frame is terminal and emitted
once: succeeded, failed, cancelled, expired, or ambiguous / no_effect when the
transport could not prove what it changed. Branch a failure on failureClass,
a closed set; errorCode is the transport's own open vocabulary.
Every frame carries id:. Save the id of the last frame you finished handling
and reconnect with Last-Event-ID set to it: the stream replays what followed,
at most 500 per batch, as far back as 90 days. A frame can arrive twice across
a reconnect, so apply frames by id. An error frame means the key or the
project stopped working; do not reconnect in a loop on it.
Frame names are command, message, error, and on request receipt,
delivery_failed, location (?events=locations), moderation (a contact's edit
or unsend, ?events=moderation) and poll (?events=polls). Ignore unknown event
names. There are no reaction, conversation or typing frames: a contact's
tapback is the reaction.added or reaction.removed webhook. A thread's past is
GET /v1/conversations/{conversationId}/messages.
A message frame's text can be null (an attachment alone); fetch each id in
attachmentIds with GET /v1/attachments/{id}. externalId is the transport's id
for the message. replyToExternalId is always Mapier's own message id, never
the transport's. On a live line a chat background set or removed in a
one-to-one conversation also arrives as a message frame, and a
message.received webhook, with null text: skip a received message that has no
text and no attachment rather than reply to it.
Send an Idempotency-Key on every command, and the SAME key on every retry of
it. A request without one gets a key minted for that request alone, so its
retry is a second command. A repeated key is adopted on every command except
group.create, typing.start and message.read on a live line, where it is silently ignored and
a group.create retry creates a second group. Do not write one retry wrapper that assumes all commands are
idempotent. A key names one command: never reuse it for a different one. A key
is bound to its command forever: reuse it for an in-flight retry, but use a NEW
key for a deliberate resend after the command has settled, or the call is
adopted and nothing is sent.
To react to a message, send reaction.tapback. A group chat is
a conversation: its messages arrive under the group's own conversationId with
isGroup true, and a message.send to that id reaches every member (every kind
but location_request). group.update (rename, add_participant or
remove_participant, one per command; set_photo is refused) and group.leave work
only in a group chat. A leave cannot be undone, and its succeeded means sent,
not proven; a leave whose sending errored settles ambiguous with
group_leave_unverified, so check on a phone before leaving again. Right after a group's members change, a
group.update, a group.leave or a message sent into it settles failed with
stale_target until its next message. With a test key there is no group chat:
a group.update or group.leave is 422 at target.conversationId.
group.create's command frame carries a null conversationId. reaction.tapback
and name_photo.share need a conversationId the contact has written into, with
a live key or a test one. A tapback names its message at
payload.targetMessageExternalId, by the externalId its message frame carried
or by the message's own messageId where the contact sent it.
chat_background.set (payload.preset, one of the published presets: gradient,
the color_*, aurora_*, sky_*, water_* and glitter_* names its page lists, or
custom with payload.colors, exactly two "#RRGGBB" colours and refused with any
other preset) and chat_background.remove (an empty payload) work in a one-to-one conversation
the transport has confirmed, or in a group chat. Give a set and a remove each
its own Idempotency-Key: on a live line one key reused across the two is
answered with the first command and changes nothing more. A settlement that is
ambiguous with chat_background_unverified may or may not have made the change,
and the API cannot read a background back: ask someone in the conversation
before sending it again. A set that settles failed with
capability_constraint_violation named a preset the line's device does not
carry: nothing changed, and gradient is on every device that sets a background.It is also worth telling an agent that there is no client SDK — every
example in these docs is raw fetch or curl, and the typed surface is the
OpenAPI document at https://api.mapier.ai/openapi.json. Left to itself, a
model will happily invent an npm install @mapier/imessage step. Tell it,
too, that results arrive on the stream or on a webhook and never in the 202.