Zrzur Developers

Gateway

The gateway is a WebSocket that pushes events to your bot. Keep one connection open per bot process.

You will need

Node 22 or newer has WebSocket built in. Any WebSocket client works. You need your bot token (YOUR_BOT_TOKEN).

Connect

wss://events.zrzur.com/?version=1&format=json
Query parameterValuesDefault
version11
formatjson, msgpackjson
tokenyour bot token, instead of Authenticatenone
intentsthe sum of your intents, instead of the field in Authenticatesee below
readyfields to include in Ready, e.g. ready=servers&ready=channelseverything

With format=msgpack both sides send binary frames.

Prefer Authenticate to ?token=: addresses end up in logs. Send it within 15 seconds:

{ "type": "Authenticate", "token": "YOUR_BOT_TOKEN", "intents": 33 }

The server answers, in this order:

  1. Authenticated: the token is good.
  2. Ready: your starting data. Events flow only after this one.

Anything else you send is limited to 1 MiB per message.

When the gateway refuses you

What you receiveCauseWhat to do
{ "type": "Error", "data": { "type": "InvalidSession" } }, closesWrong or reset token, suspended bot, or no Authenticate in 15 secondsStop. Do not reconnect in a loop
{ "type": "Error", "data": { "type": "MissingIntent", "intent": "SERVER_MEMBERS" } }, closes with code 4014A privileged intent that is off. The close reason names it, for example Disallowed intents: MESSAGE_CONTENT (switched on: none)Switch it on in the Developer Portal and connect again, or connect without it
{ "type": "Error", "data": { "type": "InternalError" } }, closes with code 1013The server could not read the bot's settings just thenConnect again, with the wait from Reconnect

Stay connected

Send a Ping every 20 seconds:

{ "type": "Ping", "data": 1730000000000 }

The server answers Pong with the same data. data is a number, or a list of bytes.

The Zrzur gateway itself never closes a quiet bot connection, but proxies and home routers do, and a dead line gives no sign. So the ping is how you find out: when no Pong came for 60 seconds, close the socket and connect again.

Reconnect

A connection can end at any time: a deployment on our side, a network problem, your laptop sleeping. Plan for it.

  1. When the socket closes, wait 1 second, and double the wait after each failure, up to 30 seconds. Add a little randomness so many bots do not return at once.
  2. Connect and send Resume if the session was resumable (see Resume after a drop), Authenticate otherwise. After Authenticate you get a new Ready.
  3. Reset the wait after Ready or Resumed.
  4. Do not retry after an Error event of type InvalidSession, or after code 4014: the same request fails again. Stop and tell a person. After code 1013 do retry: the server could not read the bot's settings just then.

Resume after a drop

A session can pick up where it stopped, so a short drop loses nothing. Ask for it when you authenticate:

{
  "type": "Authenticate",
  "token": "YOUR_BOT_TOKEN",
  "intents": 33,
  "resumable": true
}

Ready then carries session_id. Every packet after Ready carries seq, counted per session from 1: keep the last one you handled. A Bulk packet has one seq and its items have none. Authenticated, Ready, Pong, Error, InvalidSession and Resumed have no seq.

After a drop the gateway keeps the session for 2 minutes, with at most its last 1000 events and 4 MiB of them. Open a new socket and send this as the first message, instead of Authenticate:

{
  "type": "Resume",
  "token": "YOUR_BOT_TOKEN",
  "session_id": "YOUR_SESSION_ID",
  "seq": 41
}
What you receiveMeaningWhat to do
The missed events in order, then ResumedThe session goes on. There is no new ReadyCarry on, and keep counting seq
{ "type": "InvalidSession", "resumable": false }The session is gone, or what you missed no longer fits in what is keptSend Authenticate on the same socket, then catch up over REST

A replay can have gaps in seq: typing events are not kept, and events from places the bot can no longer see are left out. The gateway keeps at most 3 dropped sessions per bot. A second refused Resume on the same socket closes it.

Without "resumable": true nothing is kept, and events sent while you were away are gone. After a new Ready, fetch over REST what your bot cannot miss, such as recent messages of a channel (GET /channels/{target}/messages with after and sort=Oldest, see Messages). A command used while your bot was away fails for the person who used it, and they can try again.

Sharding is not supported

A connection receives everything your intents cover. There is no way to split servers between connections. A bot process that grows too large cannot be divided yet; keep the work you do per event small, and move slow work to a queue. See Running in production.

Two connections with the same token both work, and both receive everything. An InteractionCreate then reaches both, so only run two during a restart.

Bulk packets

Several events can arrive in one packet:

{
  "type": "Bulk",
  "v": [{ "type": "ServerMemberUpdate" }, { "type": "InteractionCreate" }]
}

Always unpack Bulk and handle each event. A bot that ignores it misses events, including command uses.

What you can send

MessageFields
Authenticatetoken, intents, resumableYour first message. token is the bot token, intents the sum of the intents you want. With resumable: true the session can be resumed after a drop, and Ready carries session_id.
Resumetoken, session_id, seqThe first message on a new socket after a drop, instead of Authenticate. The events after seq that you may still see arrive in order, then Resumed. If the session is gone you get InvalidSession.
BeginTypingchannelShows the bot as typing in channel. Send it again every few seconds to keep it up.
EndTypingchannelStops the typing display in channel.
Subscribeserver_idAsk for presence and member updates of server_id. Bots need SERVER_MEMBERS or PRESENCE to see those updates.
Pingdata, respondedKeeps the line tested. data is a number of your choice and comes back in Pong.

Intents

Intents say what a connection receives. The gateway applies them, so a bot never gets what it did not ask for. Add the numbers of the ones you want and send the sum. Without intents the bot gets all free intents, 63.

IntentValueReceives
SERVERS1Server, server channel, role, emoji and webhook events; servers and their channels in Ready. Direct messages and groups arrive either way, in Ready and as ChannelCreate
SERVER_MESSAGES2Messages and typing in server channels
DIRECT_MESSAGES4Messages and typing in direct messages and groups
REACTIONS8Reactions being added and removed
VOICE_STATES16People joining and leaving voice channels, calls, stage changes, soundboard plays, voice states in Ready
INTERACTIONS32InteractionCreate
MESSAGE_CONTENT1024Privileged. Text, embeds, files and components of messages
SERVER_MEMBERS2048Privileged. Members joining, leaving and changing; the member list
PRESENCE4096Privileged. Who is online, and their status

A bot that only answers commands needs SERVERS + INTERACTIONS = 33. Without INTERACTIONS your bot cannot hear commands.

Privileged intents are switched on for the bot in the Developer Portal. Asking for one that is off closes the connection with code 4014. Past 100 servers, only the privileged intents Zrzur approved stay on: the others are left out of the connection without closing it, and the REST rules below apply.

The same rules hold over REST:

A change of a bot's intents takes effect on its next connection.

When a privileged intent is off

A switch in the Developer Portal is saved the moment you flip it, and the portal says so. The gateway reads the switches when a connection starts, so an open connection keeps the intents it started with: close the socket and connect again. The token stays the same. REST reads them on every request, so REST answers follow a switch at once.

MissingIntent names the intents that are off. If your bot can work without one, connect again without it instead of stopping:

const MESSAGE_CONTENT = 1024;
// SERVERS + SERVER_MESSAGES + DIRECT_MESSAGES + INTERACTIONS + MESSAGE_CONTENT
let intents = 1 + 2 + 4 + 32 + MESSAGE_CONTENT;

function connect() {
  const socket = new WebSocket("wss://events.zrzur.com/?version=1&format=json");
  socket.addEventListener("open", () =>
    socket.send(
      JSON.stringify({
        type: "Authenticate",
        token: process.env.ZRZUR_BOT_TOKEN,
        intents,
      }),
    ),
  );
  socket.addEventListener("message", ({ data }) => {
    const event = JSON.parse(data);
    if (event.type === "Error" && event.data.type === "MissingIntent")
      console.warn(`Switched off in the Developer Portal: ${event.data.intent}`);
  });
  socket.addEventListener("close", ({ code }) => {
    if (code === 4014 && intents & MESSAGE_CONTENT) {
      // go on without message text: mentions and direct messages keep theirs
      intents &= ~MESSAGE_CONTENT;
      connect();
    }
  });
}

connect();

Without MESSAGE_CONTENT your bot still reads messages that mention it, its own messages and direct messages with it. Add the reconnect loop from Reconnect for every other close.

Machine-readable schema

GET /gateway/schema on the API returns every event (server) and everything a client can send (client) as JSON Schema, with the intents. It is generated from the types the gateway uses.

Full example

The complete bot has all of this: intents, ping, watchdog, Bulk, backoff and the two cases where it stops.

Next