Skip to content
Developers
Menu

Gateway

The gateway is a WebSocket that pushes events to your bot as they happen. Reading and writing data stays on REST.

On this page

Connecting

Open a WebSocket to:

Text
wss://api.palloo.app/v1/gateway
  • Send no token in the URL and no Authorization header. You authenticate inside the connection.
  • Every frame is a JSON text frame. Binary frames are refused, and frames you send may be at most 64 KiB.
  • Too many connection attempts from one address answer 429 rate_limited (60 at once, then 1 a second). A server process that is shutting down answers 503 gateway_unavailable.

Frames

Every frame has the same envelope:

JSON
{"op": "dispatch", "t": "message_create", "d": {}}
Field Meaning
op The kind of frame.
t The event name, only when op is dispatch.
d The payload, absent when the op has none.
Op Direction Payload
hello from Palloo {"heartbeat_interval_ms": 30000, "protocol_version": 2, "ops": [...]}. Always the first frame.
identify from you {"token": "palloo_bot_..."}. Must be your first frame, within 10 seconds.
ready from Palloo The session snapshot, once, after a successful identify.
heartbeat from you No payload. Send one every heartbeat_interval_ms.
heartbeat_ack from Palloo No payload. The answer to each heartbeat.
dispatch from Palloo An event, named by t.
update_presence from you {"status", "afk", "custom_status"}, each optional.
subscribe_presence from you {"community_ids": [...]}, at most 5. Needs the presence intent.
request_online_members from you {"community_id", "offset", "limit"}. Needs the presence intent; answered with online_members.

Ignore ops, events and fields you do not recognize. New ones are added without a version change. Send an op only if the ops list in hello names it: during a deploy you may reach an older server process, and an op it does not know closes the connection with 4001.

The session

  1. Open the WebSocket. Palloo sends hello.
  2. Send identify with your bot token.
  3. Palloo answers with ready, or closes the connection with a close code.
  4. Send heartbeat every heartbeat_interval_ms (30 seconds). A connection that sends nothing for 1.5 intervals is closed with 4005. If heartbeat_ack stops arriving, reconnect.
  5. Receive dispatch frames until the connection closes.

ready holds your bot's user (with kind: "bot"), every server it is in as communities (each with its channels, roles and active threads), its dm_channels, and protocol_version and ops again. If the servers do not fit in about 1 MiB, ready carries the first ones and the rest follow at once as community_create events, before any other event. Treat them exactly like servers listed in ready.

A minimal bot in JavaScript

Node.js 22 or newer has WebSocket built in. This connects, identifies, keeps the heartbeat going, prints messages and reconnects with backoff:

bot.mjs
const token = process.env.PALLOO_BOT_TOKEN;
let attempt = 0;

function connect() {
  const ws = new WebSocket('wss://api.palloo.app/v1/gateway');
  let heartbeat;

  ws.addEventListener('message', (event) => {
    const frame = JSON.parse(event.data);

    if (frame.op === 'hello') {
      ws.send(JSON.stringify({ op: 'identify', d: { token } }));
      heartbeat = setInterval(() => ws.send(JSON.stringify({ op: 'heartbeat' })), frame.d.heartbeat_interval_ms);
    } else if (frame.op === 'ready') {
      attempt = 0;
      console.log(`Ready as ${frame.d.user.username} in ${frame.d.communities.length} servers`);
    } else if (frame.op === 'dispatch' && frame.t === 'message_create') {
      console.log(`${frame.d.author.username}: ${frame.d.content}`);
    }
  });

  ws.addEventListener('close', (event) => {
    clearInterval(heartbeat);
    if (event.code === 4003 || event.code === 4007) {
      console.error(`Closed with ${event.code}, not reconnecting.`);
      return;
    }
    const delay = event.code === 4008 ? Math.random() * 2000 : Math.min(60000, 1000 * 2 ** attempt++) * (0.5 + Math.random() / 2);
    setTimeout(connect, delay);
  });
}

connect();

The same in Python

Python has no WebSocket client in its standard library. This uses the websockets package (pip install websockets):

bot.py
import asyncio
import json
import os

import websockets

TOKEN = os.environ["PALLOO_BOT_TOKEN"]


async def heartbeat(ws, interval):
    while True:
        await asyncio.sleep(interval)
        await ws.send(json.dumps({"op": "heartbeat"}))


async def main():
    async with websockets.connect("wss://api.palloo.app/v1/gateway") as ws:
        hello = json.loads(await ws.recv())
        await ws.send(json.dumps({"op": "identify", "d": {"token": TOKEN}}))
        beat = asyncio.create_task(heartbeat(ws, hello["d"]["heartbeat_interval_ms"] / 1000))
        try:
            async for raw in ws:
                frame = json.loads(raw)
                if frame["op"] == "ready":
                    print("Ready as", frame["d"]["user"]["username"])
                elif frame["op"] == "dispatch" and frame["t"] == "message_create":
                    print(frame["d"]["author"]["username"], frame["d"]["content"])
        finally:
            beat.cancel()


asyncio.run(main())

Add reconnecting with backoff, as in the JavaScript example, before you run it for real.

Reconnecting and catching up

There is no resume. Every reconnect is a new identify, and any server process can take it. To catch up on what you missed:

  1. Reconnect with backoff and identify again.
  2. For each channel in ready, compare last_message_id with the newest message you have. If they differ, page through GET /channels/{id}/messages?after=<newest id you have> until a page comes back shorter than limit.
  3. Replace your servers, channels, roles and DMs with what ready says, together with the community_create events that follow it. Anything missing was deleted, or the bot lost access.

Events can arrive that ready already reflects. Deduplicate messages by id, and order them by id, not by arrival: IDs sort in creation order.

Events

Payloads use the REST shapes from the API reference. The events a bot is most likely to use:

Event Payload
message_create Message, with nonce if the sender gave one
message_update Message
message_delete {"id", "channel_id", "community_id"} (no community_id in a DM)
message_delete_bulk {"ids", "channel_id", "community_id", "last_message_id"}
channel_create, channel_update Channel, or a DM channel
channel_delete {"id", "community_id"}
channel_pins_update {"channel_id", "community_id", "last_pin_timestamp"}
community_create A server with channels and roles, as in ready. Sent when the bot is added to one
community_update Community
community_delete {"id"}. The server was deleted, or the bot left or was removed
member_add, member_update Member plus community_id
member_remove {"community_id", "user"}
role_create, role_update Role
role_delete {"id", "community_id"}
role_positions {"community_id", "roles": [{"id", "position"}]}
interaction_create InteractionCreate. Only the command's bot receives it; see Slash commands
thread_create, thread_update Channel of kind public_thread or private_thread
thread_delete {"id", "community_id", "parent_id", "kind"}
typing_start {"channel_id", "community_id", "user_id", "started_at"}
voice_state_update VoiceState
user_update The user's public profile after a change
presence_update {"user_id", "status", "custom_status", "community_id"}, only with the presence intent

Palloo only sends events for channels the bot can view. When a role or overwrite change takes access away, the bot receives channel_delete for that channel; when it gains access, channel_create. Without Read Message History in a channel, message_update and message_delete arrive only for messages sent after the connection loaded the server.

Message content

Unless its owner turned on Read message content for it, a bot receives message_create and message_update in servers with content set to "", except for its own messages and messages that mention it directly. Everything else in the payload (author, mentions, IDs, times) is unchanged, and messages in DMs with the bot always carry their content. The same rule applies to message history over REST. Turning the setting on or off applies to open connections at once.

Presence intent

Bots set their own status with update_presence; custom_status.text is shown as their activity. A bot only receives other people's presence when its owner turned on the presence intent: then every presence_update of the members of its servers arrives without subscribing, ready.presences is filled, subscribe_presence sends presence_list snapshots and request_online_members is answered. Without it, the bot gets no presence at all and those two ops are ignored. A server can also turn off presence for bots altogether (bot_presence_allowed).

There are no other intents. Apart from presence, a bot receives the events a member in its place would, for the servers and channels it can see. Moderation report events only ever go to people.

Close codes

Code Meaning What to do
1000 The connection ended normally. Reconnect if you still want events.
1009 A frame you sent was over 64 KiB. Fix the bot, then reconnect.
4000 A problem on Palloo's side. Reconnect with backoff.
4001 Not JSON text, or an op Palloo does not know. Fix the bot, then reconnect with backoff.
4002 The first frame was not a valid identify, or you sent it twice. Fix the bot, then reconnect with backoff.
4003 The token is invalid or was revoked: the token was regenerated or the bot deleted. Do not reconnect with the same token.
4004 No identify within 10 seconds. Reconnect and identify promptly.
4005 Nothing received for 1.5 heartbeat intervals. Reconnect.
4006 You fell too far behind (256 undelivered events). Reconnect at once and catch up over REST.
4007 Too many connections: the bots of one owner share 10 per server process, and the oldest was closed. Do not reconnect automatically; you would close another of your connections.
4008 The server process is shutting down for a deploy. Reconnect at once, with a little random delay.
4009 update_presence, subscribe_presence or request_online_members had a payload Palloo cannot accept. Fix the bot, then reconnect with backoff.
4010 The server process lost the link that carries events between processes, so events may have been missed. Reconnect with backoff and catch up over REST.
4011 Terms of service not accepted. Never sent to bots.

Backoff means exponential with jitter, starting around 1 second and capped around 1 minute. Treat a code you do not know as "reconnect with backoff".

Limits

Limit Value
Time to identify 10 seconds
Heartbeat interval 30 seconds; closed after 45 seconds of silence
Frame size you can send 64 KiB
Undelivered events per connection 256
Connections of all the bots of one owner 10 per server process together; the oldest is closed
ready size about 1 MiB; further servers follow as community_create
Connection attempts per address 60 at once, then 1 a second
update_presence per connection 5 at once, then 1 every 10 seconds; extra updates are combined and applied later
subscribe_presence per connection 5 at once, then 1 every 2 seconds; at most 5 servers

The server re-checks a connection's token, memberships and channel access about every 2 minutes as a backstop. Changes normally apply at once.