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.send

Sends 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 at target.conversationId when 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, effect and subject, at payload.<key>, and a reply, at target.replyToMessageId;
  • on the conversation path, an effect outside the thirteen names or a subject outside 1 to 1024 characters, at payload.<key>, and a reply to a message that carries no iMessage id — one the contact did not send and whose command frame carried no externalId — at target.replyToMessageId;
  • a media link, a sticker, a rich_link, a multipart, a location_request or a poll into any other thread, at target.conversationId or the recipient key you wrote, with reason: conversation_not_confirmed — send the person a text first;
  • a location_request into 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.lineId that is on another channel than target.channel names, or is not the line a conversationId names — 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 a 403 line_not_allowed, not a 422.

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

Linemessage.send carriesEvery other payload key
iMessage, live and testtext — 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 options422 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.

FieldDefaultRangeMeaning
x, y0.5−5 to 6The sticker's centre, as a fraction of the bubble from its top-left corner. Outside 0–1 hangs off the bubble.
scale10.1 to 5Multiplier of the standard size, about 48 pt wide for an emoji.
rotation0−180 to 180Degrees, 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:

  • emoji that is not exactly one emoji ("👍👍", "👍 ", a word, a digit), at payload.emoji. Nothing is trimmed.
  • placement without attachToMessageExternalId, a placement value that is not a number or is out of range, or a partIndex outside 0 to 64, at that field.
  • A reply, target.replyToMessageId, beside attachToMessageExternalId.
  • Both emoji and url, or neither; any of the keys without kind: "sticker"; and a transferId, which /v1 never 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 at payload.url.
  • A thread the transport has neither confirmed nor reported as a group chat, at target.conversationId or the recipient key you wrote, with reason: 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.

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

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

event: command — succeeded
{
  "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.

event: command — the handle could not be reached
{
  "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_failed when 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.recipientExternalId or whichever of its spellings you used — target.conversationId, invalid_text as the entry's reason, or conversation_not_confirmed as the entry's reason for 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, a media link the platform cannot use at payload.url — media_fetch_refused, media_fetch_failed, media_too_large or unsupported_media_type as the entry's reason — or a caption at payload.caption, or a multipart's count or link at payload.parts and its part at payload.parts[i].url or payload.parts[i].text). details names the field — see What a line carries.
  • 403 recipient_not_allowed when the contact opted out, is blocked or paused, or a channel's service window has closed. details.reason says which: opted_out, blocked, paused or outside_service_window.
  • 403 line_not_allowed when target.lineId is 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.reason is not_owned and details.lineId the id you sent. GET /v1/whoami lists the lines the key can use.
  • 503 no_line_capacity or 503 line_unavailable about 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 error message about that line rather than about every line on the project.
  • 409 conflict when the Idempotency-Key is taken and there is nothing to adopt. details.reason says how: idempotency_key_in_flight (the first send is still running), idempotency_key_failed (it was refused), idempotency_key_unresolved (its outcome is unknown) or idempotency_key_reused (another command holds the key).
  • 503 service_busy with retryAfter when 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_unavailable when the transport did not take the send, or answered with something unusable — on the conversation path, with reason: no_live_connector while the line's device is offline. The send was recorded and settled first, so its key is spent: a same-key retry answers 409 conflict, and a resend goes under a new key — after checking the conversation when details.reason is timeout, network or status_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 as details.reason (or a 422 entry's reason) under the published code, never as the code.

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.

On this page