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.
Envelope
Section titled “Envelope”{ "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.
Client → server messages
Section titled “Client → server messages”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.
Server → client events
Section titled “Server → client events”Event names below are the canonical constants from the client’s WSEventType registry and match backend emissions exactly.
Direct messages
Section titled “Direct messages”| 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 |
Presence
Section titled “Presence”| 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.
Channels
Section titled “Channels”| 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 |
Groups
Section titled “Groups”| 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.
One-to-one calls
Section titled “One-to-one calls”| 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.
Group calls
Section titled “Group calls”| 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 |
Stories and polls
Section titled “Stories and polls”| 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
Section titled “Live location”live_location_started, live_location_update, live_location_stopped — relayed E2EE; the server never sees coordinates.
Encrypted metadata
Section titled “Encrypted metadata”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 |
System
Section titled “System”| 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.
Payload examples
Section titled “Payload examples”Representative payloads for the highest-traffic events.
message
Section titled “message”{ "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.
read_receipt
Section titled “read_receipt”{ "type": "read_receipt", "payload": { "from": "0x1a2b…", "message_id": "msg_01H…", "read_status": "read", "timestamp": 1760000000 }}read_status is one of delivered, read, or seen.
typing
Section titled “typing”{ "type": "typing", "payload": { "from": "0x1a2b…", "typing": true }}The server clears stale typing state automatically: if a user disconnects mid-typing, peers receive typing: false.
online
Section titled “online”{ "type": "online", "payload": { "address": "0x1a2b…", "online": true }}reaction_added
Section titled “reaction_added”{ "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).
encrypted_call_signal
Section titled “encrypted_call_signal”{ "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.
device_sync
Section titled “device_sync”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).
