message.send
Send text into a conversation, or start a new one with a recipient — with an effect, a subject and a quoted reply once the contact has written, and a photo by link, a sticker, a rich link, text and photos as one message, a request for the contact's location, or a poll.
POST /v1/commands/message.sendSends a message. What the payload may carry is decided by the line the send
resolves to, per channel. An iMessage line — live or test — carries text, and body is read as a
second spelling of text; on a conversation the transport has
confirmed or reported as a group chat, effect, subject and textFormatting beside it; and six kinds in place of the text: a photo or file by link (media), an emoji sticker, a rich_link, a multipart — text and photos as one message — a location_request, Apple's "Share My Location?" card, and a poll. A key set to null, an object
holding only nulls and an empty attachments or media list say nothing and
are skipped. Any other key that carries a value is refused with
422 validation_failed before anything is queued, and details names the key
it stopped at. Nothing is dropped with a 202. See
What a line carries.
Target
Accepts a conversation target (reply into an existing thread) or a
recipient target (start a new one). The platform reads the keys, not the
kind: a target carrying a conversationId is a conversation target, and one
carrying an address instead is a recipient target, whatever kind says.
A message someone writes in a group arrives in the group's own conversation —
its message frame says isGroup: true — and an iMessage line sends into it
on the conversation path, to every member. See Groups.
Prop
Type
Which path a send takes
On a live line the thread, not the target, chooses the transport's path, and the path decides what the payload may carry.
The conversation path. It is taken by a thread the transport has confirmed
is a conversation with that one contact — which it does when it reports the
conversation, the first time the contact writes into it — and by a group chat
the transport reported (for every kind but location_request), whichever
target named the thread. It carries effect and subject beside text,
quotes a reply named at target.replyToMessageId, honours Idempotency-Key,
and is the one path for the kinds the recipient path cannot carry: media, sticker, rich_link, multipart, location_request and poll. A command the line's device has not taken within 30 seconds expires —
its command frame settles expired, failureClass: not_run; while the device
is offline the send is refused at once, 503 upstream_unavailable with
reason: no_live_connector; and while the transport's sending is paused the
send is accepted, 202, and its command then expires.
The recipient path. Any other thread — a recipient send to somebody who
has never written to the line, or a thread whose contact has not answered yet —
takes it, so a conversation can still be opened by writing first. It lets a
command wait five minutes and has three limits. The message is text alone,
which the transport refuses once the message is written, with invalid_text as
the entry's reason, when blank or over 16,384 bytes (16 KiB) rather than
the 65,536 the body allows — that message is recorded as failed, so its key is
spent: send the corrected text under a new key. Nothing but text reaches the
handset — no effect, subject, quoted reply or attachment. And the transport
does not honour Idempotency-Key: the platform records every send against its
key before the line is asked, so a retry is never posted twice, but a send
whose answer was lost leaves its message needs_reconcile rather than being
sent again, and its command frame carries the transport's own
logicalActionId.
What a path cannot carry is refused synchronously, 422 validation_failed
before anything is written — never a message sent without it:
- on the recipient path, an email address, at the target field you wrote it
at —
target.recipientExternalId, or whichever of its spellings you used — and attarget.conversationIdwhen it is the peer of a thread you named that takes the recipient path; that path sends to an E.164 phone number; - on the recipient path,
effectandsubject, atpayload.<key>, and a reply, attarget.replyToMessageId; - on the conversation path, an
effectoutside the thirteen names or asubjectoutside 1 to 1024 characters, atpayload.<key>, and a reply to a message that carries no iMessage id — one the contact did not send and whosecommandframe carried noexternalId— attarget.replyToMessageId; - a
medialink, asticker, arich_link, amultipart, alocation_requestor apollinto any other thread, attarget.conversationIdor the recipient key you wrote, withreason: conversation_not_confirmed— send the person a text first; - a
location_requestinto a group chat, the same way — no text changes that: send it into a member's own one-to-one conversation; - on either path, a group conversation the transport has not named yet, at
target.conversationId; - a line named at
target.lineIdthat is on another channel thantarget.channelnames, or is not the line aconversationIdnames — a conversation stays on its line. A command done to a thread — a tapback, a read,typing.start— naming a line the person is not on is refused there too, until a message has opened a conversation with them on that line. An id that is not one of your lines at all is a403 line_not_allowed, not a422.
Honours Idempotency-Key on either target: the platform records every
message.send against its key, per project and environment, before the line
is asked, so a retry is never posted twice whichever target it names. It is
adopted when a transport took the first attempt; when none did it is refused
with 409 conflict — see Errors. The transport honours the key
itself on the conversation path and not on the recipient path, which is why a
recipient-path send whose answer was lost leaves its message needs_reconcile
rather than being sent again.
Payload
Text
The default. No kind field. effect, subject and textFormatting are
optional companions on the conversation path, and a 422 at payload.<key> on
the recipient path.
Prop
Type
{ "text": "hello" }{ "text": "You are in. See you Saturday.", "subject": "Saturday", "effect": "confetti" }What a line carries
| Line | message.send carries | Every other payload key |
|---|---|---|
| iMessage, live and test | text — and, on a confirmed conversation or a group chat, effect, subject and textFormatting beside it; kind: "media" with an https link and no caption; kind: "sticker" with emoji or an image link, attachToMessageExternalId, partIndex and placement; kind: "rich_link" with url; kind: "multipart" with parts (an effect and a subject beside them, as on a text); kind: "location_request" with nothing beside it; kind: "poll" with question and options | 422 validation_failed at payload.<key>; nothing is queued |
That 422 carries the line's channel and the keys it carries in the same
entry. A photo or file goes by link — kind: "media" with an https url of at most 2048 characters and no caption (a 422 at payload.caption), fetched by the platform before anything is written and refused at payload.url when it cannot use the link — or as the parts of a multipart, refused at payload.parts (the count, a link twice) or at the part (payload.parts[i].url, payload.parts[i].text) before anything is written; and a sticker's transferId, which /v1 never takes — an image sticker goes by url — or any of a sticker's keys without kind: "sticker", is a 422 at that key. Formatting outside its rules is a 422 at payload.textFormatting, or at the range, payload.textFormatting[i].
A reply names a message in the same conversation at target.replyToMessageId,
beside the payload and never in it. On the conversation path it is quoted when
it names a message the contact sent or one of yours whose command frame
carried an externalId; any other reply, and every reply on the recipient
path, is 422 validation_failed at target.replyToMessageId before anything
is written — never a message sent unquoted.
The thread is refused at its field the same way, before anything is written:
an email address at target.recipientExternalId — or whichever of its
spellings you used — and at target.conversationId for a thread whose peer is
one; a media link, a sticker, a rich_link, a multipart, a location_request or a poll into a thread the transport has neither confirmed nor reported as a group chat — or a location_request into a group chat — at target.conversationId or the recipient key, with reason: conversation_not_confirmed; and a line named at target.lineId that is on another channel than target.channel names or is not the line a conversationId names (one that is not yours at all is a 403 line_not_allowed). Which path a send takes
has the whole rule, and Choosing the line says how a named line meets the line a person is already on.
media
A photo or file by link, on the conversation path alone; the guide is Attachments.
Prop
Type
{ "kind": "media", "url": "https://example.com/orders/4821/receipt.pdf" }Nothing else goes beside url: a caption is a 422 at payload.caption, and
an effect or a subject at that key. While your project, or the platform, is
already sending as many files as it can at once the send is 503 service_busy
with retryAfter; neither that nor a refused link writes a message, so the same Idempotency-Key may be sent again — unlike a 503 upstream_unavailable after the transport was asked, after which the key is spent. The media keys are checked for shape before
the line is known — url or caption without kind: "media", a link that is
not https — so that 422 names the key and carries no channel. A test line
fetches nothing: it refuses a media link at payload.url in a confirmed
conversation, and elsewhere with reason: conversation_not_confirmed.
sticker
One emoji, or an image by https link, drawn as a picture — on its own as a bubble, or stuck onto a message the contact sent at a spot you choose on that bubble. It travels on the conversation path alone, so it goes only into a thread the transport has confirmed or reported as a group chat. The guide is Stickers.
Prop
Type
placement fields are plain JSON numbers, each optional, each rounded to 4
decimal places before it is checked; an omitted one keeps its default.
| Field | Default | Range | Meaning |
|---|---|---|---|
x, y | 0.5 | −5 to 6 | The sticker's centre, as a fraction of the bubble from its top-left corner. Outside 0–1 hangs off the bubble. |
scale | 1 | 0.1 to 5 | Multiplier of the standard size, about 48 pt wide for an emoji. |
rotation | 0 | −180 to 180 | Degrees, positive clockwise. |
With no placement, the sticker lands dead centre and covers a short message.
Pick a corner instead:
{
"kind": "sticker",
"emoji": "👍",
"attachToMessageExternalId": "5D1B7E0A-2F3C-4B8E-9A61-1C2D3E4F5A6B",
"placement": { "x": 0.95, "y": 0.1, "rotation": 12 }
}These are refused with 422 validation_failed at the field named, before
anything is written:
emojithat is not exactly one emoji ("👍👍","👍 ", a word, a digit), atpayload.emoji. Nothing is trimmed.placementwithoutattachToMessageExternalId, aplacementvalue that is not a number or is out of range, or apartIndexoutside 0 to 64, at that field.- A reply,
target.replyToMessageId, besideattachToMessageExternalId. - Both
emojiandurl, or neither; any of the keys withoutkind: "sticker"; and atransferId, which/v1never takes — at that key. - An image link that is not a PNG, GIF or JPEG, is over 512,000 bytes or more than 618 px on a side — at
payload.url; the file is fetched inside the send and reaches no device. A test line does not rehearse an image sticker and refuses one atpayload.url. - A thread the transport has neither confirmed nor reported as a group chat,
at
target.conversationIdor the recipient key you wrote, withreason: conversation_not_confirmed.
A sticker to a line on any other channel is 404 unsupported_command with
reason: not_on_channel.
What the line can draw is not known in advance
A line whose device has not announced emoji stickers — or placement, for a placed sticker, or
replies on a sticker, for a reply — is sent nothing: the command is accepted, 202, and then
settles failed with capability_constraint_violation, failureClass: capability. It never
falls back to a centred or unplaced sticker. See Capabilities.
rich_link
A URL sent as a preview card the recipient's phone draws, on the conversation path alone. The guide is Rich links.
Prop
Type
{ "kind": "rich_link", "url": "https://example.com/launch" }Nothing else goes beside url: another key is a 422 at that key. A reply is
target.replyToMessageId, as on any send. iMessage answers a rich link with
no message id, so the command frame's externalId is null and no later
command — a tapback, a sticker stuck to it, an edit or an unsend — can name it.
A line whose device has not announced rich links — or replies on one, for a
reply — is sent nothing: 202, then failed with
capability_constraint_violation, failureClass: capability.
multipart
Text and photos as one message, in the order given — a caption over a photo, an album, or text and photos interleaved — on the conversation path alone. The guide is Multipart messages.
Prop
Type
{
"kind": "multipart",
"parts": [
{ "kind": "text", "text": "Here they are:" },
{ "kind": "media", "url": "https://example.com/listings/4821/front.jpg" },
{ "kind": "media", "url": "https://example.com/listings/4821/kitchen.jpg" }
]
}Every link is fetched and uploaded to the line before the message is sent, in
order; one that fails refuses the whole send at that part's url, never a
partial message. Audio is not a part — iMessage sends it as a message of its
own. A reply is target.replyToMessageId, as on any send. A line whose device
has not announced multipart sends is sent nothing: 202, then failed with
capability_constraint_violation, failureClass: capability — never several
separate messages. On a test line a multipart of text parts is performed and
one holding a media part settles failed with unsupported_capability,
since the sandbox fetches nothing.
location_request
Apple's "Share My Location?" card, asking the contact to share their location with the line — on the conversation path alone, so only into a one-to-one conversation the transport has confirmed. The guide is Location.
Prop
Type
{ "kind": "location_request" }Nothing goes beside kind: any other key, a text included, is 422 validation_failed at that key before anything is written. A reply is
target.replyToMessageId, as on any send. A group, or a conversation not yet
confirmed as with that one person, is a 422 with
reason: conversation_not_confirmed.
succeeded means the card was sent, never that the person shared. If they tap
Share, their location arrives later, apart from this command, as
location events. A line that has not announced
location requests — or replies on one, for a reply — is sent nothing: 202,
then failed with capability_constraint_violation (or
capability_not_negotiated), failureClass: capability. On a test line the
card is sent and delivered, and nobody shares back.
poll
A question with 2 to 12 choices that the people in the conversation tap to vote on — on the conversation path alone. The guide is Polls.
Prop
Type
{ "kind": "poll", "question": "Lunch?", "options": ["Tacos", "Ramen", "Pho"] }Nothing else goes beside question and options: any other key, a text
included, is 422 validation_failed at that key before anything is written,
and so is a question or options beside any other kind. The 128-character
bound is set so that every poll sent comes back with its ids. A reply is target.replyToMessageId, as on any send.
Into a thread off the conversation path it is a 422 with
reason: conversation_not_confirmed.
The poll's own id and the ids its choices were given arrive as a poll.created
with actor: "line" on a stream opened with ?events=polls — the ids
poll.vote names. The command frame carries the poll's id
as externalId when the transport's settlement names one. A line
that has not announced polls is sent nothing: 202, then failed with
capability_constraint_violation (or capability_not_negotiated),
failureClass: capability. On a test line the poll is sent and comes back as
poll.created, and nobody on the test phone votes on it.
Request
curl https://api.mapier.ai/v1/commands/message.send \
-H "Authorization: Bearer $MAPIER_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-2481-shipped" \
-d '{
"target": {
"kind": "conversation",
"conversationId": "a0000000-0000-4000-8000-000000000001"
},
"payload": { "text": "Your order shipped." }
}'Starting a new conversation instead:
{
"target": { "kind": "recipient", "channel": "imessage", "recipientExternalId": "+14155550100" },
"payload": { "text": "hi" }
}Response
{
"commandId": "cmd-1",
"status": "queued",
"disposition": "queued",
"lineId": "0192a7b0-3e4f-7051-8c6d-7e8f9a0b1c2d",
"lineSelection": "reused_active_chat"
}lineId is the line the send went out on, as GET /v1/whoami
lists it, and lineSelection says why that one: named when the target named
it at target.lineId, reused_active_chat when it is the conversation's own
line, the line of the message a reply answers, or the line the person last
wrote to, and
new_best_line when the person is new to the project and the platform picked
one. See The shape of every response.
Then, on the response stream:
{
"commandId": "cmd-1",
"logicalActionId": "order-2481-shipped",
"status": "succeeded",
"errorCode": null,
"conversationId": "a0000000-0000-4000-8000-000000000001",
"failureClass": null
}succeeded is the transport's settlement, not a delivery receipt — receipts
are their own frames.
{
"commandId": "cmd-1",
"logicalActionId": "order-2481-shipped",
"status": "failed",
"errorCode": "send_returned_not_ok",
"conversationId": "a0000000-0000-4000-8000-000000000001",
"failureClass": "target"
}Errors
A command answers the one set of HTTP refusals
POST /v1/commands/{type} publishes — every type the
same codes, service_busy and no_line_capacity (capacity_exhausted from
2026-10-15) among them — so that table
is the one to handle. What is particular to this type:
422 validation_failedwhen a field is wrong or missing, when the payload carries a key the line does not (payload.<key>), or when a live iMessage line cannot carry the address, the group, the text or the kind (the target field you wrote the address at —target.recipientExternalIdor whichever of its spellings you used —target.conversationId,invalid_textas the entry'sreason, orconversation_not_confirmedas the entry'sreasonfor asticker, arich_link, amultipart, alocation_requestor apollinto a thread the transport has neither confirmed nor reported as a group chat, or alocation_requestinto a group chat, a media link the platform cannot use atpayload.url—media_fetch_refused,media_fetch_failed,media_too_largeorunsupported_media_typeas the entry'sreason— or acaptionatpayload.caption, or a multipart's count or link atpayload.partsand its part atpayload.parts[i].urlorpayload.parts[i].text).detailsnames the field — see What a line carries.403 recipient_not_allowedwhen the contact opted out, is blocked or paused, or a channel's service window has closed.details.reasonsays which:opted_out,blocked,pausedoroutside_service_window.403 line_not_allowedwhentarget.lineIdis not a line of this project in the key's environment — another project's, the other environment's, or nobody's; the API does not say which.details.reasonisnot_ownedanddetails.lineIdthe id you sent.GET /v1/whoamilists the lines the key can use.503 no_line_capacityor503 line_unavailableabout the line you named, when it is full, paused, released or its connection is down — the same codes a send to that line meets, with an errormessageabout that line rather than about every line on the project.409 conflictwhen theIdempotency-Keyis taken and there is nothing to adopt.details.reasonsays how:idempotency_key_in_flight(the first send is still running),idempotency_key_failed(it was refused),idempotency_key_unresolved(its outcome is unknown) oridempotency_key_reused(another command holds the key).503 service_busywithretryAfterwhen your project, or the platform, is already sending as many files by link as it can at once — nothing is written, and the same key may be sent again.503 upstream_unavailablewhen the transport did not take the send, or answered with something unusable — on the conversation path, withreason: no_live_connectorwhile the line's device is offline. The send was recorded and settled first, so its key is spent: a same-key retry answers409 conflict, and a resend goes under a new key — after checking the conversation whendetails.reasonistimeout,networkorstatus_5xx, since then the send may have landed.- On an iMessage line the transport's own reasons for refusing a send — such as
invalid_text— reach you asdetails.reason(or a422entry'sreason) under the published code, never as thecode.
Settlement codes you will see for this command: invalid_target,
gateway_rejected, send_returned_not_ok, outbound_echo_unconfirmed; on the
conversation path, expired (failureClass: not_run) for a command the line
did not take within 30 seconds, stale_target for a reply the line no longer
holds or for a group addressed right after its membership changed, before its
next message, capability_constraint_violation or capability_not_negotiated (failureClass: capability) for a sticker, a rich link, a multipart, a location request or a poll the line cannot send, and, on a test line, unsupported_capability for a multipart holding a media part. See Error codes.
Related
Text and rich text
Effects, subject lines, replies and byte limits in practice.
Attachments
A photo or file by link, and receiving the ones people send you.
Stickers
One emoji, placed on a message — and what a line that cannot draw one answers.
Rich links
A URL as a preview card, and why nothing can name the message afterwards.
Multipart messages
Text and photos as one message, every link fetched before anything is sent.
Location
Ask the contact to share their location, and what comes back.
Idempotency and retries
When a retry is adopted, and when it is a new command.