Groups

Group chats through /v1 — creating one, sending into one, renaming it, changing its members and leaving it, and what each outcome tells you.

On an iMessage line a group chat is a conversation too. A message someone writes in a group arrives under the group's own conversationId, with isGroup: true. Send to that id and the message reaches every member. group.update renames the group or adds or removes one member, and group.leave takes the line out of it — both only in a group chat. group.create starts a new one.

What counts as a group chat is a conversation the transport reported as a group. A group conversation the transport has not named yet is 422 validation_failed at target.conversationId, and nothing is written.

group.create is not retry-safe

On a live line group.create accepts an Idempotency-Key header and then ignores it — the line mints a fresh identity for every call. Retrying a group.create that appeared to time out creates a second group. Wait for the settlement on the response stream before you decide whether anything needs retrying. Every other group command honours the key.

Where a group's id comes from

A group's conversationId reaches you on the message frames from the group: every message someone writes there arrives under the group's id, and its from is the person who wrote it. Store that id the way you store a one-to-one conversation's.

A group.create's own command frame carries conversationId: null, because it opens the thread the transport then reports. The group's id reaches you once someone writes in it.

Creating a group

iMessage has no concept of an empty group, so group.create creates the chat and sends its opening message in one operation. firstMessage is required, is trimmed, and runs from 1 to 16,384 bytes. There is no way to make a group and then decide what to say.

The target is a new_group naming 2 to 32 other people at participantExternalIds, each an E.164 phone number or a lower-case Apple ID email of at most 512 characters. Do not include yourself — you are implied — and order does not matter. The platform reads each participant the way the channel reads an address and checks the list before anything is sent, in test and live alike, each refusal a 422 validation_failed: a participant the channel cannot read is refused at target.participantExternalIds[i] (or target.participants[i], if you wrote that key); fewer than 2 or more than 32 distinct people — one person written two ways counts once — is refused at target.participantExternalIds. The reference has every case.

curl https://api.mapier.ai/v1/commands/group.create \
  -H "Authorization: Bearer $MAPIER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "target": {
      "kind": "new_group",
      "channel": "imessage",
      "participantExternalIds": ["+14155550100", "+14155550111"]
    },
    "payload": { "firstMessage": "Kicking off the thread here." }
  }'

If you are not sure every participant is on iMessage, check the addresses first — see Checking reachability. A participant who cannot receive iMessages does not fail the request in a way you can see at the boundary.

In test the sandbox cannot create a group: a group.create whose participants pass the check above is accepted and settles failed / unsupported_capability.

What its settlement tells you

A group.create settles like any other command, as a command frame correlated by commandId. Its conversationId is null — present, not omitted:

event: command
{
  "commandId": "cmd-1",
  "logicalActionId": "cmd-1",
  "status": "succeeded",
  "errorCode": null,
  "conversationId": null,
  "failureClass": null
}

There is one more settlement to handle. group.create checks its own work before claiming success: it looks for the new chat, compares its participants against the set you asked for, and looks for your firstMessage in its history. When it cannot prove all three, its command frame settles ambiguous with group_content_unverified — meaning the group may well exist, but nothing confirmed it. That is the case where a blind retry does real damage, because a second group.create makes a second group.

In practice that means writing the handler so every path is covered:

if (event.status === "succeeded") {
  await recordGroupCreated(event.commandId);
} else if (event.status === "ambiguous" || event.status === "no_effect") {
  // May or may not exist. Do not retry automatically — check first.
  await markGroupNeedsReview(event.commandId);
} else {
  await recordGroupFailed(event.commandId, event.failureClass, event.errorCode);
}

Sending into a group

A message.send to a group's conversationId goes on the transport's conversation path and reaches every member. It carries what that path carries — effect and subject beside text, a quoted reply named at target.replyToMessageId, and every kind but location_request — and honours Idempotency-Key. A location_request into a group is 422 validation_failed at target.conversationId with reason: conversation_not_confirmed, and no text changes that: send it into a member's own one-to-one conversation.

curl https://api.mapier.ai/v1/commands/message.send \
  -H "Authorization: Bearer $MAPIER_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: trip-planning-reminder-1" \
  -d '{
    "target": {
      "kind": "conversation",
      "conversationId": "a0000000-0000-4000-8000-000000000002"
    },
    "payload": { "text": "Flights land at 6. Who is on the airport run?" }
  }'

Renaming a group and changing its members

group.update takes one action per command:

{ "action": "rename", "name": "Trip planning" }
{ "action": "add_participant", "participantExternalId": "+14155550122" }

remove_participant takes the same shape. A name is at most 128 characters, not blank and without an ASCII control character; a participant is one E.164 phone number or one lower-case Apple ID email of at most 512 characters. Each is refused at its own field, 422 validation_failed, before anything is sent. set_photo is refused at payload.action: a group photo is not carried yet.

Three outcomes are worth planning for:

  • A new group refuses member changes. A group the transport has seen no message in yet refuses an add_participant or a remove_participant at once, 409 conflict with details.reason: "group_has_no_messages_yet". Send a message into it first. A rename is not held back.
  • A change that does not apply. Adding someone already in the group, or removing someone who is not, settles failed with command_claim_invalid — judged against the members as the transport last saw them. A line that cannot change a group's members settles an add or a remove failed with capability_constraint_violation, and changes nothing.
  • A change nobody could read back. succeeded means the device read the change back. ambiguous with group_update_unverified means it sent the change and could not read it back: check the group's name or members on a phone in it before you send the change again.

Leaving a group

group.leave takes the line out of the group. Its payload is empty.

A leave needs a group of four, and is not proven

Apple lets a member leave only a group of at least 4 people, the line among them. In a smaller group a group.leave is sent, changes nothing and still settles succeeded. Nothing reads a leave back either: succeeded means the leave was sent without an error, not that it took effect, and a leave whose sending errored settles ambiguous with group_leave_unverified. After either, check on a phone in the group before you send a leave again. A leave that did happen cannot be undone.

A group's background

chat_background.set and chat_background.remove set and remove the background of a group chat the transport has reported, as they do a confirmed one-to-one conversation's. A set names its background at payload.preset; a remove takes an empty payload.

In a group chat the line's own change files nothing. In a conversation with one person a change also arrives as a message with no text — see Inbound message events.

After membership moves

A group addressed right after its membership changed — a member added or removed — and before its next message settles failed with stale_target instead of running. That holds for a group.update, a group.leave, a background change and a message sent into the group alike.

The fix is not a blind retry. Wait for the next message frame from the group, then send again under a new Idempotency-Key: the settled command holds the first one, so a resend under it would be adopted and send nothing.

This is a safety property, not a bug to work around. Resolving a group means finding the chat whose members match what the transport expects. When the two disagree, the command refuses to run rather than guess — a wrong guess would send your message, or your change, to the wrong set of people.

In test

A test line has no group chat. A well-formed group.update or group.leave on a test key is 422 validation_failed at target.conversationId, never a 202; a malformed payload is refused at its field there as on a live line. To see a group change land, use a live line. There is no group chat to set a background in either.

On this page