<- Documentation - WebSocket Events
Note
This page documents the Gateway WebSocket (/subscribe on port 3100) used for chat, presence, and subscriptions. The SFU WebSocket (/signal on port 3300) is a separate connection used exclusively for voice/WebRTC signaling — see SFU Protocol for its message format.
All messages exchanged over the Gateway WebSocket share a single JSON envelope:
{
"op": 0,
"d": { ... },
"t": 100
}| Field | Type | Required | Description |
|---|---|---|---|
op |
int | ✅ | Operation code — determines how the message is routed |
d |
object / json | ✅ | Payload data (structure varies by op + t) |
t |
int | ❌ | Event type — only meaningful when op = 0 (Dispatch) or op = 7 (RTC). Omitted for control ops |
Note: The server accepts both
"d"and"data"as the payload key (for client convenience).Note: Some Dispatch payloads are personalized per recipient. For example, message
nonceis present only in the author's own live message event and is stripped from the same channel event delivered to other users.
| OP | Name | Description | Payload |
|---|---|---|---|
| 1 | Hello | First message after WS connect; authenticates the session | See Hello |
| 2 | Heartbeat | Keep-alive sent on the server-defined interval | See Heartbeat |
| 3 | Presence Update | Update own presence status | See Presence Update |
| 5 | Channel Subscription | Subscribe to channel and/or guild events | See Channel Subscription |
| 6 | Presence Subscription | Manage which users' presence you track | See Presence Subscription |
| 7 | RTC | WebRTC/voice signaling (send to SFU or keep-alive) | See RTC |
| OP | Name | Description |
|---|---|---|
| 0 | Dispatch | Server-push event (message created, guild updated, etc.). Always includes t field |
| 1 | Hello Reply | Response to Hello — contains heartbeat interval and session ID |
| 3 | Presence Update | Dispatched presence snapshot for a subscribed user (includes voice state: mute/deafen) |
| 7 | RTC | Voice/WebRTC events (offer, candidate, speaking, mute, kick, etc.) |
Sent immediately after WebSocket upgrade. If not received within 5 seconds, the server closes the connection.
{
"op": 1,
"d": {
"token": "eyJhbGci...",
"heartbeat_session_id": "optional-previous-session-id"
}
}| Field | Type | Required | Description |
|---|---|---|---|
token |
string | ✅ | JWT access token (from /auth/login or /auth/refresh) |
heartbeat_session_id |
string | ❌ | Session ID from a previous connection to resume presence session |
Server response (OP 1):
{
"op": 1,
"d": {
"heartbeat_interval": 30000,
"session_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
}
}| Field | Type | Description |
|---|---|---|
heartbeat_interval |
int64 | Heartbeat interval in milliseconds |
session_id |
string | UUID-style session ID; pass as heartbeat_session_id on reconnect |
On success the server also:
- Fetches user profile and guild list in parallel.
- Subscribes the connection to the personal
user.{userId}NATS topic. - Subscribes to all user's guilds (
guild.{guildId}). - Starts the heartbeat timeout timer.
Must be sent periodically per the heartbeat_interval from Hello. Missing heartbeats (with 10s grace) cause disconnection.
{
"op": 2,
"d": {
"e": 42
}
}| Field | Type | Description |
|---|---|---|
e |
int64 | Last event ID acknowledged by the client (monotonically increasing) |
The server resets the heartbeat timer only if e >= lastEventId. On each heartbeat, the server also refreshes the session's presence TTL in Redis (throttled to at most once per 10 seconds).
Update own presence status. Presence is not auto-set on Hello — the client must explicitly send this op.
{
"op": 3,
"d": {
"status": "online",
"platform": "web",
"custom_status_text": "Coding...",
"voice_channel_id": 2230469276416868352,
"mute": false,
"deafen": false
}
}| Field | Type | Required | Description |
|---|---|---|---|
status |
string | ✅ | "online", "idle", "dnd", or "offline" (invisible mode) |
platform |
string | ❌ | "web", "mobile", "desktop" — informational only |
custom_status_text |
string | ❌ | Free-text status message |
voice_channel_id |
int64 | ❌ | Set to a channel ID to indicate voice presence; set to 0 to clear |
mute |
bool | ❌ | Set to true/false to update mute status while in voice channel |
deafen |
bool | ❌ | Set to true/false to update deafen status while in voice channel |
Behavior:
"offline"sets a global override — the user appears offline to all watchers, even though the session is active.- Any other valid status clears the offline override and upserts the session.
- The aggregated presence is published to NATS (
presence.user.{userId}) for all presence subscribers. - When
muteordeafenis provided and the user is in a voice channel, the voice state is updated and a Voice State Update event (t=209) is broadcast to all guild members.
Voice State Update Flow:
Client A → OP 3 (mute: true) → WS Service
↓
Update presence
↓
Broadcast t=209 to guild.{guildId}
↓
All guild members receive voice state update
See Event Types for the Voice State Update event details.
Subscribe to channel-specific events (typing, messages) and/or additional guild topics.
{
"op": 5,
"d": {
"channels": [2226022078341972000, 2226022078341972001],
"guilds": [2226022078304223200]
}
}| Field | Type | Required | Description |
|---|---|---|---|
channels |
int64[] | ❌ | Exact channel ID list to subscribe to (guild channels, DMs, or group DMs) |
channel |
int64 | ❌ | Legacy one-channel fallback; treated as channels: [channel] when channels is absent |
guilds |
int64[] | ❌ | Additional guild IDs to subscribe to |
Permission checks for channel subscriptions:
- Guild channel — must have
PermServerViewChannelson that channel. - DM channel — must be a participant.
- Group DM — must be a participant.
If none match, the subscription is silently rejected (logged server-side).
When channels is present, it replaces the connection's entire explicit channel-subscription set. Send channels: [] to clear all channel subscriptions.
For threads:
- include the thread ID in
channelsif this connection should receive live thread message events - thread membership is managed separately through the REST thread-member routes and controls personal notifications / unread behavior
Manage which users' presence updates you receive in real-time.
{
"op": 6,
"d": {
"add": [123456789, 987654321],
"remove": [111111111],
"set": [222222222, 333333333],
"clear": false
}
}| Field | Type | Description |
|---|---|---|
add |
int64[] | User IDs to start watching |
remove |
int64[] | User IDs to stop watching |
set |
int64[] | Replace the entire watch list with these IDs |
clear |
bool | If true, unsubscribe from all presence currently watched |
Processing order: clear → set → add → remove.
When a user ID is added (via add or set), the server immediately sends a presence snapshot for that user so the client has the current status without waiting for a change.
Used for voice-related control over the Gateway WS connection. Only t=509 (RTCBindingAlive) is sent client → Gateway. The full RTC signaling (join, offer, answer, candidate, speaking, mute, etc.) happens on the separate SFU WebSocket (/signal on port 3300) — see SFU Protocol.
RTCBindingAlive (t=509) — Keep voice route alive:
{
"op": 7,
"t": 509,
"d": {
"channel": 2230469276416868352
}
}This refreshes voice:route:{channelId} TTL (60s) in Redis and updates the session's voice channel presence. Clients should send this periodically while in a voice channel.
When you subscribe to a user's presence via OP 6, you receive their presence updates via OP 3 dispatches:
{
"op": 3,
"d": {
"user_id": 2226021950625415200,
"status": "online",
"custom_status_text": "In a meeting",
"since": 1700000000,
"voice_channel_id": 2230469276416868352,
"mute": true,
"deafen": false,
"client_status": {
"web": "online"
}
}
}| Field | Type | Description |
|---|---|---|
user_id |
int64 | User whose presence changed |
status |
string | Current status: online, idle, dnd, or offline |
custom_status_text |
string | User's custom status message |
since |
int64 | Unix timestamp when this status was set |
voice_channel_id |
int64 | Voice channel ID if in voice (omitted if not in voice) |
mute |
bool | Whether the user is muted (only present if in voice channel) |
deafen |
bool | Whether the user is deafened (only present if in voice channel) |
client_status |
map | Per-platform status (e.g., web, desktop, mobile) |
Voice State Fields:
mute: true— User has muted themselves (cannot speak)deafen: true— User has deafened themselves (cannot hear others)- These fields are only included when the user is currently in a voice channel
- They are updated via OP 3 (client → server) and then broadcast to all presence subscribers
Server-push events with t field indicating event type. See Event Types for complete list.
Example — Voice State Update (t=209):
{
"op": 0,
"t": 209,
"d": {
"guild_id": 2226022078304223200,
"user_id": 2226021950625415200,
"channel_id": 2230469276416868352,
"mute": true,
"deafen": false
}
}This event is broadcast to all guild members when any user's voice state (mute/deafen) changes.