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:
{
"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_participantor aremove_participantat once,409 conflictwithdetails.reason: "group_has_no_messages_yet". Send a message into it first. Arenameis not held back. - A change that does not apply. Adding someone already in the group, or
removing someone who is not, settles
failedwithcommand_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 removefailedwithcapability_constraint_violation, and changes nothing. - A change nobody could read back.
succeededmeans the device read the change back.ambiguouswithgroup_update_unverifiedmeans 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.
Related
group.update
Rename a group chat, or add or remove one member.
group.leave
Take the line out of a group chat. It cannot be undone.
group.create
Parameters, errors and the settlement shape.
chat_background.set
Set a conversation's background, a group chat's included.
Addressing a conversation
Where conversation ids come from.