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.send to a recipient target validates the address as E.164 and answers 422 recipient_not_allowed otherwise. Email handles work as group participants and as a handle-check address, but not here.
  • Plain text only, and it degrades silently. The endpoint forwards payload.text and drops every other key, so a recipient send carrying an effect or a reply target returns 202 and 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-Key is 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

KindFormatValidInvalid
PhoneE.164, leading +, 7–15 digits+141555501004155550100, (415) 555-0100
EmailLowercase, NFC-normalisedperson@example.comPerson@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.

On this page