Skip to content

Event catalog

Every frame on the WebSocket connection is a JSON envelope with a type string and a payload object. This page lists all event types the server emits and the client-to-server message types it accepts.

{
"type": "message",
"payload": { }
}

Unknown type values are ignored by both server and reference client, so new event types can ship without breaking existing consumers. Always dispatch on exact type strings.

These are the message types the server’s read loop handles. Everything else you send is dropped (and logged as unknown).

Type Purpose
authenticate Redundant backup auth after subprotocol authentication; safely ignored if already authenticated.
ping Application-level heartbeat; server replies with pong.
typing Plaintext typing indicator. Refused with MESH_ONLY_FORBIDDEN in mesh-only mode — use encrypted_typing.
read_receipt Plaintext read receipt. Refused with MESH_ONLY_FORBIDDEN in mesh-only mode — use encrypted receipts.
call_offer, call_answer, ice_candidate, call_reject, call_end Plaintext WebRTC signaling, relayed to the peer. Subject to WebRTC rate limits.
encrypted_call_signal E2EE call signaling; the server relays the encrypted blob without decrypting it.
live_location_started, live_location_update, live_location_stopped E2EE live location sharing; server relays encrypted coordinates.
subscribe_bucket Sealed-sender v3 routing: { "bucket_id": number, "prev_bucket_id": number? }.

The server sets the authoritative from address on relayed frames — a client cannot spoof another sender.

Event names below are the canonical constants from the client’s WSEventType registry and match backend emissions exactly.

Event Payload summary
message New direct message (see JSON below)
voice_message New voice message
read_receipt { from, message_id, read_status, timestamp }
typing { from, typing }
message_edited { message_id, chat_id, new_content, … } plus optional E2EE fields
message_deleted { message_id, chat_id, … } plus optional E2EE fields
reaction_added { message_id, chat_id, emoji, from, … } plus optional E2EE fields
reaction_removed Same shape as reaction_added
disappearing_timer_changed Disappearing-message timer changed for a chat
message_pinned / message_unpinned Message pinned or unpinned in a chat
Event Payload summary
online { "address": string, "online": boolean }
status_update { "address": string, "status": string }
profile_update { address, first_name, last_name, bio, avatar_chunk_id }

On connect, the server sends you one online event per currently connected user (excluding blocked relationships). In mesh-only deployments plaintext presence events are suppressed entirely; use the encrypted presence events instead.

Event Trigger
channel_message New channel message
channel_created Channel created
channel_updated Channel metadata updated
channel_deleted Channel deleted
channel_member_joined A member joined the channel
channel_member_left A member left the channel
channel_member_promoted A member was promoted
channel_member_removed A member was removed
channel_message_edited Channel message edited
channel_message_deleted Channel message deleted
channel_message_pinned Channel message pinned
channel_message_unpinned Channel message unpinned
channel_message_view_count_updated View count changed on a message
channel_reaction_added Reaction added to a channel message
channel_reaction_removed Reaction removed from a channel message
Event Trigger
group_message New group message
group_created Group created
group_updated Group metadata updated
group_deleted Group deleted
group_member_joined A member joined the group
group_member_left A member left the group
group_member_role_changed A member was promoted or demoted
group_member_removed A member was removed
group_message_edited Group message edited
group_message_deleted Group message deleted
group_reaction_added Reaction added to a group message
group_reaction_removed Reaction removed from a group message

group_member_promoted also exists but is deprecated; the backend emits group_member_role_changed for both promotions and demotions.

Event Payload summary
call_offer { from, to, sdp, call_type, call_id }
call_answer { from, to, sdp, call_id }
ice_candidate { from, to, candidate, call_id }
call_reject { from, to, reason, call_id } — reason: busy, declined, unavailable
call_end { from, to, reason, call_id } — reason: hangup, timeout, error, disconnect
call_renegotiation_offer { from, to, offer_sdp, call_id }
call_renegotiation_answer { from, to, answer_sdp, call_id }
encrypted_call_signal E2EE signaling envelope (see JSON below)

Note the exact backend names: ice_candidate, call_reject, call_end — not call_ice_candidate, call_rejected, or call_ended. See Calls and WebRTC for the full signaling flow.

Event Trigger
group_call_invite Invitation to a group call
group_call_offer SDP offer from a participant
group_call_answer SDP answer from a participant
group_call_ice_candidate ICE candidate from a participant
group_call_participant_joined A participant joined the call
group_call_participant_left A participant left the call
group_call_muted A participant muted
group_call_unmuted A participant unmuted
group_call_screen_share_started A participant started screen sharing
group_call_screen_share_stopped A participant stopped screen sharing
group_call_ended The group call ended
Event Trigger
story_created New story published
story_deleted Story deleted
story_viewed Someone viewed your story
poll_created New poll created
poll_vote_update Vote counts changed
poll_closed Poll closed

live_location_started, live_location_update, live_location_stopped — relayed E2EE; the server never sees coordinates.

Server-blind events: the server relays these without being able to read their contents.

Event Purpose
encrypted_presence_update Presence update the server cannot read
encrypted_presence_request Request a peer’s encrypted presence
encrypted_read_receipt Read receipt without revealing which message was read
encrypted_typing Typing indicator without revealing typing state
Event Payload summary
sync_ack Sync acknowledgement
member_update / subscriber_update Membership counters changed
user_action Nested action envelope (sub-types below)
account_deleted { address, username, deleted_at } — sent to chat partners
device_sync Own-device sync echo (see below)
pong Reply to a client ping; may carry timestamp
error { type, code, message } in-band error frame

user_action sub-types: user_blocked, user_unblocked, you_were_blocked, you_were_unblocked, user_muted, user_unmuted, chat_cleared, verification_request, verification_confirmed.

Representative payloads for the highest-traffic events.

{
"type": "message",
"payload": {
"id": "msg_01H…",
"from": "0x1a2b…",
"content": "",
"timestamp": 1760000000,
"sequence_number": 42,
"reply_to_id": "msg_01G…",
"is_forwarded": false,
"encrypted": true,
"e2ee_header": "base64…",
"e2ee_ciphertext": "base64…"
}
}

When encrypted is true, content is empty and the payload carries the Double Ratchet e2ee_header and e2ee_ciphertext (plus e2ee_initial_message with X3DH handshake data for new sessions). sequence_number is monotonic for guaranteed ordering.

{
"type": "read_receipt",
"payload": {
"from": "0x1a2b…",
"message_id": "msg_01H…",
"read_status": "read",
"timestamp": 1760000000
}
}

read_status is one of delivered, read, or seen.

{
"type": "typing",
"payload": { "from": "0x1a2b…", "typing": true }
}

The server clears stale typing state automatically: if a user disconnects mid-typing, peers receive typing: false.

{
"type": "online",
"payload": { "address": "0x1a2b…", "online": true }
}
{
"type": "reaction_added",
"payload": {
"message_id": "msg_01H…",
"chat_id": "chat_01H…",
"emoji": "👍",
"from": "0x1a2b…",
"is_encrypted": false
}
}

When is_encrypted is true, emoji is a placeholder and the real reaction is in encrypted_payload (with e2ee_header for DMs or e2ee_nonce for groups).

{
"type": "encrypted_call_signal",
"payload": {
"from": "0x1a2b…",
"to": "0x3c4d…",
"call_id": "call_01H…",
"signal_type": "offer",
"encrypted_signal": "base64…",
"e2ee_header": "base64…",
"e2ee_version": 1
}
}

signal_type is one of offer, answer, ice, rejected, ended, renegotiation_offer, renegotiation_answer. The server relays the blob without decrypting it and overwrites from with the authenticated sender.

Echoed only to the sender’s other devices so they can mirror local chat history. Never carries plaintext.

{
"type": "device_sync",
"payload": {
"kind": "sent_message",
"origin_device_id": "dev_01H…",
"sent_at": "2026-07-14T12:00:00Z",
"to": "0x3c4d…",
"ciphertext": "base64…"
}
}

kind is one of sent_message, edit (with message_id and new ciphertext), delete (with delete_message_id), or read (with chat_id, last_read_at, and optional last_read_message_id).