跳到正文
CalabDocs

开发者文档目前仅提供英文版。

Gateway: realtime events

Connecting to the gateway over WebSocket, IDENTIFY and READY, heartbeats, events, RESUME after a drop and close codes.

The gateway is a persistent WebSocket connection over which the server pushes events to the bot: new messages, reactions, voice changes, tasks. One socket per bot. If a persistent connection doesn't suit you (serverless), message and task events are also available through a webhook.

Connecting

text
wss://app.example.com/gateway?v=1                  # binary protobuf (default)
wss://app.example.com/gateway?v=1&encoding=json    # protojson text frames

Every frame is GatewayFrame { op, seq, <payload> }. Frames are binary protobuf by default; with encoding=json they are JSON text frames in the same format as REST (lowerCamelCase fields, enums by full name, uint64 as strings). JSON is handy when your language has no protobuf library.

Lifecycle

  1. Server → HELLO { heartbeatIntervalMs } — the heartbeat interval, about 41 seconds.
  2. Bot → IDENTIFY { token, device: { name, platform, appVersion } } — the bot token and a description of the process.
  3. Server → DISPATCH ready with seq = 1: sessionId, me, workspaces[] (for each workspace — visible rooms, members, roles, voice states, presence, the bot's permissions per room), dms[], readStates.
  4. Then DISPATCH frames with events and a growing seq.
  5. Every heartbeatIntervalMs the bot sends HEARTBEAT { lastSeq }; the server answers HEARTBEAT_ACK. No heartbeat for 2 × the interval + 10 s closes the socket with 4009.

Frames in JSON mode:

JSON
→ {"op": "GATEWAY_OPCODE_IDENTIFY", "identify": {"token": "calab_bot_0192a1b2-…", "device": {"name": "my-bot", "platform": "linux", "appVersion": "1.0"}}}
← {"op": "GATEWAY_OPCODE_HELLO", "hello": {"heartbeatIntervalMs": 41000}}
← {"op": "GATEWAY_OPCODE_DISPATCH", "seq": "1", "dispatch": {"ready": {"sessionId": "…", "me": {"user": {"id": "…", "isBot": true}}, "workspaces": […], "dms": […]}}}
← {"op": "GATEWAY_OPCODE_DISPATCH", "seq": "2", "dispatch": {"messageCreate": {"workspaceId": "…", "message": {"id": "…", "content": "/echo hi", "command": {"botUserId": "…", "name": "echo", "args": "hi"}}}}}
→ {"op": "GATEWAY_OPCODE_HEARTBEAT", "heartbeat": {"lastSeq": "2"}}
← {"op": "GATEWAY_OPCODE_HEARTBEAT_ACK", "heartbeatAck": {}}

A minimal client

import WebSocket from 'ws';

const ws = new WebSocket('wss://app.example.com/gateway?v=1&encoding=json');
let lastSeq = '0';
let timer;

ws.on('message', (raw) => {
  const f = JSON.parse(raw.toString());
  if (f.seq) lastSeq = f.seq;
  switch (f.op) {
    case 'GATEWAY_OPCODE_HELLO':
      ws.send(JSON.stringify({ op: 'GATEWAY_OPCODE_IDENTIFY',
        identify: { token: process.env.TOKEN, device: { name: 'my-bot', platform: 'linux', appVersion: '1.0' } } }));
      timer = setInterval(() => ws.send(JSON.stringify({ op: 'GATEWAY_OPCODE_HEARTBEAT', heartbeat: { lastSeq } })),
        f.hello.heartbeatIntervalMs);
      break;
    case 'GATEWAY_OPCODE_DISPATCH':
      if (f.dispatch.messageCreate) console.log('message:', f.dispatch.messageCreate.message.content);
      break;
  }
});
ws.on('close', (code, reason) => { clearInterval(timer); console.log('closed', code, reason.toString()); });

This is a skeleton: a real bot also needs reconnects and RESUME (below). The Node.js SDK does all of that for you.

Events

An event is the dispatch field of a DISPATCH frame (exactly one per frame). A bot gets only what it can see through its permissions (VIEW_ROOM, VIEW_BOARD).

EventWhen
ready · resumedafter IDENTIFY · after a successful RESUME
messageCreate · messageUpdate · messageDeletemessages in rooms and the bot's DMs (its own included)
messageReactionAdd · messageReactionRemovereactions
botCallbacka person pressed a button under the bot's message — to that bot only; see Commands and buttons
voiceStateUpdate · voiceStreamStart / voiceStreamStop · voiceCameraStop · voiceMovedvoice: who is in which room, mute, streams
presenceUpdate · userUpdatepresence and profiles
roomCreate / roomUpdate / roomDelete · roomPermissionsUpdate · categoryCreate / categoryUpdate / categoryDeleterooms and categories
workspaceCreate / workspaceUpdate / workspaceDelete · workspaceMemberAdd / workspaceMemberUpdate / workspaceMemberRemove · roleCreate / roleUpdate / roleDeleteworkspaces, members, roles
dmCreatea new DM with the bot
typingStart“is typing” — only in rooms from SUBSCRIBE
stickerPackCreate / stickerPackUpdate / stickerPackDeletethe workspace's sticker packs
soundCreate / soundUpdate / soundDelete · soundPlaythe workspace's sounds; soundPlay only while the bot is in the room's call
botCreate / botUpdate / botDeletethe workspace's bots — only with MANAGE_BOTS
sipCallUpdatea phone call from a room was placed or changed status
eventCreate / eventUpdate / eventDelete · eventRsvpmeetings: its own (the bot organizes) and those of visible rooms; outside addresses only if the bot may change the meeting
boardCreate / boardUpdate / boardDelete · taskCreate / taskUpdate / taskDelete · taskActivityboards and tasks — by the bot's VIEW_BOARD
taskNoticea person's task notification, when they chose this bot — see Task notifications

The full table of every protocol event, marked with which ones reach bots, is in Gateway events. Bots don't receive people's read receipts, one-to-one call notifications, or notes, billing and Disk events.

What a bot can send

FrameWhat it doesLimits
HEARTBEAT { lastSeq }keeps the connection aliveevery heartbeatIntervalMs
TYPING { roomId }“bot is typing” in a roomat most once per 3 s per room
SUBSCRIBE { roomIds }subscribes to typingStart of these rooms; replaces the previous setup to 100 rooms
PRESENCE_UPDATE { status }presence status (PRESENCE_STATUS_ONLINE, …_IDLE, …_DND, …_INVISIBLE)—

TYPING, SUBSCRIBE and PRESENCE_UPDATE together: at most 10 in a row and 2 per second; extras are silently dropped. More than 50 frames per second sustained closes the socket with 4008.

Reconnect and RESUME

Connections drop — that's normal (server deploys, networks). The right response:

  1. Reconnect with exponential backoff: 1 → 30 s with jitter.
  2. After HELLO, send RESUME { token, sessionId, seq } — sessionId from ready, seq the last one you processed.
  3. The server replays what you missed (a buffer of about 5 minutes or 1000 events) and sends resumed { replayed }.
  4. If the session can't be resumed you get INVALID_SESSION { resumable: false }: after 1–5 s send IDENTIFY on the same socket and get a new ready.

A RECONNECT frame means the server itself asks you to reconnect (for example, before a deploy): close the socket and follow the steps above. Skip events whose seq is not greater than the last one you processed — after a replay they may arrive twice.

Close codes

CodeMeaningWhat to do
4000unknown errorreconnect, RESUME
4000 + replaced by a new sessionanother process connected with the same tokendon't reconnect
4001 · 4002unknown opcode · decode errorreconnect, RESUME
1006the connection droppedreconnect, RESUME
4003not identifiedreconnect, new IDENTIFY
4004invalid tokendon't reconnect
4007 · 4009invalid seq · heartbeat timed outreconnect, new IDENTIFY
4008rate limited (flooding, or the 256-frame send queue overflowed)back off, RESUME
4010token revoked or bot deleteddon't reconnect

Socket limits

WhatValue
Sockets per token1
Inbound frame sizeup to 64 KiB
Server send queue256 frames; overflow → 4008
RESUME bufferabout 5 minutes or 1000 events