Response stream

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."

On this page