Addressing a conversation
The three target shapes, and how to get the conversation id you need.
Every command has a target that says who it acts on. There are three
shapes, and picking the right one is usually the first question you answer.
conversation — reply into an existing thread
The one you will use most.
{ "kind": "conversation", "conversationId": "a0000000-0000-4000-8000-000000000001" }conversationId is a Mapier UUID, not an iMessage GUID. You get it from
the message event on the response stream:
{
"conversationId": "a0000000-0000-4000-8000-000000000001",
"from": "+14155550100",
"text": "is it here yet?",
"externalId": "guid-1",
"replyToExternalId": null,
"occurredAt": "2026-08-24T18:03:11.000Z",
"attachmentIds": []
}Store that id against your own records — a customer, a ticket, a thread — and you can reply at any time. A DM's id is stable for the life of the thread. A group's is resolved against its membership and can go stale — see Conversation ids go stale below.
Works for both direct messages and groups. Some commands accept only one or the other; each reference page says which.
A conversation id from another account returns conversation_not_found — the same response as an
id that does not exist. If you are getting this unexpectedly, check the id came from your own
stream.
recipient — start a new thread
When you have a phone number but no conversation yet.
{ "kind": "recipient", "recipientExternalId": "+14155550100" }Prop
Type
Three limits apply, and all three surprise people:
- Phone numbers only. Unlike every other place the API names a person, this
one does not accept an email address:
message.sendto a recipient target validates the address as E.164 and answers422 recipient_not_allowedotherwise. Email handles work as group participants and as a handle-checkaddress, but not here. - Plain text only, and it degrades silently. The endpoint forwards
payload.textand drops every other key, so a recipient send carrying an effect or a reply target returns202and delivers the bare text with those parts missing. Nothing tells you they were ignored. Rich content needs a conversation target. The text is also trimmed and capped at 16,384 bytes here, a quarter of what a conversation send accepts. - No idempotency. Recipient sends take the admin routing path, where an
Idempotency-Keyis accepted and ignored. A retry sends a second message.
The usual pattern is therefore: send a plain text opener, wait for the
conversation to appear on the stream, then use its conversationId for
everything after.
Check reachability first if you are unsure the address is on iMessage — POST /v1/handle-check answers synchronously and saves you a message that goes
nowhere.
new_group — create a group
Only group.create accepts this.
{
"kind": "new_group",
"participantExternalIds": ["+14155550100", "+14155550111"]
}Between 2 and 32 participants, each unique, each either an E.164 phone number or
a lowercase email address — unlike a recipient target, email is accepted here.
Do not include yourself. Order does not matter.
Only the "at least two, all unique" half is checked synchronously, as
422 invalid_participants. A list longer than 32 is accepted with 202 and
settles failed on the stream.
Address formats
| Kind | Format | Valid | Invalid |
|---|---|---|---|
| Phone | E.164, leading +, 7–15 digits | +14155550100 | 4155550100, (415) 555-0100 |
| Lowercase, NFC-normalised | person@example.com | Person@Example.com |
Addresses that fail these rules are rejected with 400 invalid_address on
handle check, or 422 on a send. Email is accepted everywhere a handle appears
except a recipient send target, which takes phone numbers only.
Conversation ids go stale
A group's id is resolved against its membership at the time you use it. Change
the membership — add or remove someone — and commands issued against the old
id settle stale_target.
The fix is to wait for a new message in the group, which carries a fresh
conversationId, then use that. See Groups.