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.