Response stream

Edit and unsend events

When a bubble is edited or unsent — separate frames, not a rewritten message event.

An edit or an unsend of a bubble you already have is not a new event: message and it does not delete the original frame. The stream emits event: message_edit or event: message_unsend. externalId is the original bubble's id — the same value as that message's externalId.

Your own edit or unsend still settles as event: command. These frames are the chat record. Do not treat the command frame as the new text, and do not increment anything off both.

event: message_edit
data: {"conversationId":"a0000000-0000-4000-8000-000000000001","externalId":"guid-1","text":"See you at 7","previousText":"See you at 6","editedAt":"2026-08-24T18:10:04.000Z","occurredAt":"2026-08-24T18:10:04.000Z"}
event: message_unsend
data: {"conversationId":"a0000000-0000-4000-8000-000000000001","externalId":"guid-2","unsentAt":"2026-08-24T18:12:08.000Z","occurredAt":"2026-08-24T18:12:08.000Z"}

message_edit

Prop

Type

message_unsend

Prop

Type

There is no reason field. The frame does not say whether the sender unsent the bubble or the thread changed around it. Do not infer one.

Applying a frame you may see twice

The stream is at-most-once, and replay can overlap a frame you already saw live. Recognise an edit you already applied by (externalId, editedAt, text) and an unsend by (externalId, unsentAt).

That key is how you recognise the overlap. It is not a row id. Two edits of one bubble can share an editedAt; keep both when their text differs rather than dropping one as a duplicate.

Replay does not rebuild an edit history by diffing the latest message body. If you missed the frame and the replay page does not include that row, the text you have on the original message is the text you have. The original bubble is not re-emitted, and it is not deleted, when an edit or an unsend arrives.

Catch-up

includeModeration: true on POST /v1/replay returns messageEdits[] and messageUnsends[] for that conversation — the same shapes as the live events — plus moderationTruncated. It requires conversationId. sinceExternalId does not bound it.

Each array is the newest limit rows of that kind (default 50, max 200), then reversed to chronological. moderationTruncated: true means a cap hit on either array. A 201st older row is not recoverable. Rows whose payload envelope has expired are omitted.

On this page