Conversation events
Membership and display-name snapshots. A removal is not a leave receipt.
event: conversation is a membership/name snapshot. Diff keys appear only when
the API decrypted a strictly earlier snapshot. removedParticipantExternalIds
means those handles are gone from the list Apple just reported. It is not a
leave receipt. First observation and replay-latest omit diff keys.
The stream does not emit every conversation row. Ordinary messages write the same kind. Only a material change — the ingest snapshot moved — produces a frame. Immediate leave or rename on the stream is unproved from the next bubble alone: a frame fires when that snapshot changes, which may be this action or the next message.
event: conversation
data: {"conversationId":"a0000000-0000-4000-8000-000000000002","occurredAt":"2026-08-24T18:06:00.000Z","isGroup":true,"displayName":"Trip planning","participantExternalIds":["+14155550100","+14155550111"],"addedParticipantExternalIds":[],"removedParticipantExternalIds":["+14155550122"],"displayNameChanged":false}Snapshot fields (always when we emit)
Prop
Type
Live-only diff fields
Present only when the API decrypted a strictly earlier snapshot for the
same connector, conversation and delivery epoch. Replay-latest never includes
them. First observation omits them — displayNameChanged is absent, not
false. Absence means "no proved edge," not "the name did not change."
Prop
Type
Do not name this event leave. Do not add a reason. A handle in
removedParticipantExternalIds is no longer in Apple's participant list as the
Mac just reported it. You cannot tell leave vs remove vs restamp.
Own group.leave / group.update
Those still settle as event: command. If
chat.db later shows a new participant list, a conversation frame may follow.
Two frames, two meanings.
stale_target still applies: after membership moves, the
next command against the old expectation can fail closed. Wait for a current
conversationId — from this frame, or from the next inbound message if the
snapshot only moved then.
Identity
Conversation identity is the connector plus a hash of Apple's chat GUID. If
Apple restamps that GUID, the API conversationId changes. A diff is not
computed across that boundary. Treat a new conversationId as a new thread.
Catch-up
includeConversation: true on POST /v1/replay returns
one latest snapshot: { conversationId, occurredAt, isGroup, displayName, participantExternalIds }.
No diff keys. That is not "the same shape as SSE."