Skip to content

Gateway connection

A bot receives events in real time over a WebSocket connection to the TEBHOS gateway. This page walks through the connection a bot makes; the full protocol is in the gateway reference.

Get the gateway URL

GET /gateway/bot

Returns the WebSocket URL to connect to. Authenticate with the bot token; a user token or a missing token returns 401. Rate limit: 60 per minute per client.

curl https://tebhos.com/api/v1/gateway/bot \
  -H "Authorization: Bot $TEBHOS_BOT_TOKEN"
{
  "url": "wss://tebhos.com/ws",
  "shards": 1,
  "session_start_limit": {
    "total": 1000,
    "remaining": 999,
    "reset_after": 14400000,
    "max_concurrency": 1
  }
}
Field Type Description
url string Gateway WebSocket URL. Append the query string below
shards integer Recommended shard count. Always 1
session_start_limit object Fixed informational values. They are not a live counter; the real identify limits are listed under Limits

Connection lifecycle

connect  wss://tebhos.com/ws?v=1&encoding=json
  <- Hello           (op 10)  { "heartbeat_interval": 41250 }
  -> Identify        (op 2)   { "token": "...", "properties": { ... } }
  <- READY           (op 0)   session_id, user, ...
  <- GUILD_CREATE    (op 0)   one per guild the bot is in
  -> Heartbeat       (op 1)   every heartbeat_interval ms
  <- Heartbeat ACK   (op 11)
  <- MESSAGE_CREATE  (op 0)   ... and every other event

1. Connect

Open a WebSocket to the url from /gateway/bot with these query parameters:

Parameter Value Description
v 1 Gateway version. Required; any other value, or none, closes the connection with 4012
encoding json Payload encoding. JSON is the only encoding
compress? none Transport compression. Omit it, or use none, to receive plain-text JSON frames
wss://tebhos.com/ws?v=1&encoding=json

Every frame in both directions is a JSON object:

Field Type Description
op integer Opcode
d any Payload. Frames you send must always include d, even when it is null; a frame without d closes the connection with 4001
s ?integer Sequence number, on dispatch (op 0) frames only
t ?string Event name, on dispatch (op 0) frames only

A frame you send may be at most 4096 bytes.

2. Receive Hello

The gateway immediately sends Hello (op 10) with heartbeat_interval in milliseconds (currently 41250).

3. Identify

Send Identify (op 2) with the bot token. Put the bare token in token. A leading Bot prefix, as in the REST Authorization header, is also accepted and stripped, so libraries that reuse the header value work unchanged. Bearer is not accepted.

{
  "op": 2,
  "d": {
    "token": "1290847135893127168.q3JxY2V0...W5k",
    "properties": { "os": "linux", "browser": "my-bot", "device": "my-bot" }
  }
}
Field Type Description
token string The bot token, {application_id}.{secret}
properties object Connection properties. os, browser and device are all required strings; any value is accepted
presence? object Initial presence, for example {"status": "online"}
ignored_events? array of strings Event names the gateway should not send you (at most 256), see Event filtering
flags? integer Non-negative gateway capability flags. Bots can leave this at 0
shard? array [shard_id, shard_count]. Needed only when the bot is in more than 2,500 guilds, see below

A missing or malformed field closes the connection with 4002. An invalid token closes it with 4004.

One connection can carry at most 2,500 guilds. A bot in more guilds must shard: open shard_count connections and send each one a different shard_id from 0 to shard_count - 1. A guild belongs to the shard (guild_id >> 22) % shard_count. Without a shard array, or when one shard would still hold more than 2,500 guilds, the gateway closes the connection with 4011 (Sharding required). A malformed pair closes it with 4010. /gateway/bot always recommends 1 shard, so choose shard_count yourself, with headroom above guild_count / 2500. Only shard 0 receives direct message and other events that do not belong to a guild; see The shard filter.

There are no intents: a bot receives every event, including full message content, for the guilds and channels it can see. Use ignored_events to cut down traffic you do not need.

4. Receive READY and GUILD_CREATE

When the session is ready the gateway dispatches READY. For a bot, READY carries an empty guilds array; each guild then arrives as its own GUILD_CREATE dispatch (or GUILD_DELETE with unavailable: true if a guild is temporarily unavailable).

Keep these fields from READY:

Field Use
session_id Needed to resume
user.id The bot's own user ID, to ignore its own messages

When someone adds the bot to another guild while it is connected, the gateway attaches the open session to that guild and dispatches a GUILD_CREATE for it; there is no need to reconnect.

5. Heartbeat

Send Heartbeat (op 1) every heartbeat_interval milliseconds, with d set to the last sequence number s you received (or null before the first dispatch). The gateway answers with Heartbeat ACK (op 11).

{ "op": 1, "d": 42 }
  • If the gateway sends you an op 1, send a heartbeat back immediately. It does this when it has not heard from you for about 90 % of the interval.
  • If no heartbeat arrives within 45 seconds of the last acknowledged one, the gateway closes the connection with 4009.
  • A heartbeat with a sequence number the session never sent closes the connection with 4007.
  • If you send a heartbeat and receive no ACK before the next one is due, treat the connection as dead: close it and resume.

Track s from every dispatch: it increases by one per event and is what you send in heartbeats and resumes.

Resume after a disconnect

If the connection drops, open a new one, wait for Hello, and send Resume (op 6) instead of Identify:

{
  "op": 6,
  "d": {
    "token": "1290847135893127168.q3JxY2V0...W5k",
    "session_id": "the session_id from READY",
    "seq": 42
  }
}

All three fields are required. Send token exactly as you sent it in Identify, with or without the Bot prefix: the gateway compares it with the token that opened the session. On success the gateway replays the events you missed and then dispatches RESUMED. A session can be resumed for 60 seconds after the connection drops.

If the gateway answers with Invalid Session (op 9, d: false), the session is gone: wait 1–5 seconds and send a fresh Identify. If it sends Reconnect (op 7), close the connection and resume on a new one.

Close code Meaning What to do
4000 Unknown error Resume
4001 Unknown opcode, or a frame without d Fix the payload, then identify again
4002 Decode error or invalid Identify/Resume payload Fix the payload, then identify again
4003 Sent a command before identifying Identify first
4004 Authentication failed Stop. The token is invalid or was regenerated
4005 Identify or Resume sent twice Fix your client
4007 Invalid sequence Identify again
4008 Rate limited Wait, then resume
4009 Heartbeat timeout Resume
4010 Invalid shard Fix shard, or omit it
4011 Sharding required Identify with a shard array
4012 Invalid API version Connect with v=1
4013 Acknowledgement backpressure Resume

The complete table is in Opcodes and close codes.

Set a custom status

A bot sets its presence with Presence Update (op 3), the same command the TEBHOS apps use. TEBHOS has no activities such as "Playing ..." or "Listening to ...": an activities field is ignored. A bot can show a status and a custom status, which is a short text with an optional emoji.

{
  "op": 3,
  "d": {
    "status": "online",
    "custom_status": { "text": "Type !help", "emoji_name": "🎵" }
  }
}
Field Type Description
status string Required. online, idle, dnd or invisible. offline is treated as invisible. A missing or unknown value closes the connection with 4002
afk? boolean Whether the session is away
mobile? boolean Whether the session is shown as mobile
custom_status? ?object {text?, expires_at?, emoji_id?, emoji_name?}. null clears the custom status, and omitting the key keeps the current one
  • Send it after READY. The Identify presence field accepts the same fields, see Initial presence object.
  • A Unicode emoji goes in emoji_name and is shown as is. A custom emoji (emoji_id) is published only for accounts with TEBHOS Pro, so on a bot it is normally dropped while the text is kept. An emoji_id that names no emoji rejects the whole custom status, and the previous one stays.
  • expires_at is an ISO 8601 time after which the custom status is cleared.
  • A bot whose status is invisible appears offline to everyone, without its custom status.
  • The gateway accepts 5 presence updates per 20 seconds on one connection and drops the rest without an error.

The full rules are in Presence Update.

Join a voice channel

A bot joins voice the same way the TEBHOS apps do: it asks the gateway for a place in the channel, then connects to the media server (LiveKit) with the credential the gateway returns. There is no separate HTTP route for this.

  1. Wait for the guild's GUILD_CREATE. Its voice_states show who is in which voice channel.
  2. Send Voice State Update (op 4) without a connection_id, which opens a new connection. Send IDs as strings.

    {
      "op": 4,
      "d": {
        "guild_id": "1189375284394692608",
        "channel_id": "1189375284394692610",
        "self_mute": false,
        "self_deaf": false
      }
    }
    
  3. The gateway checks View Channel and Connect, the channel's user limit and whether the bot is timed out. If the join is allowed, your session receives VOICE_SERVER_UPDATE with token, endpoint, channel_id and connection_id. Keep connection_id. Every session that can see the channel receives a VOICE_STATE_UPDATE for the bot.

  4. Connect to endpoint with token using a LiveKit SDK that runs on a server (for Node.js, @livekit/rtc-node) and publish an audio track. Without Speak the bot is admitted with suppress: true and nobody hears it.
  5. To leave, send op 4 with channel_id: null and the connection_id, then disconnect from the media server. Without connection_id the leave is refused.

    {
      "op": 4,
      "d": {
        "guild_id": "1189375284394692608",
        "channel_id": null,
        "connection_id": "the connection_id from VOICE_SERVER_UPDATE"
      }
    }
    

Things a voice bot must handle:

  • A refused join (missing permission, full channel, timed-out bot) produces no event. Wait for VOICE_SERVER_UPDATE with a timeout, for example 15 seconds.
  • A later VOICE_SERVER_UPDATE for the same guild, after a region change or a move, carries a new token. Reconnect to the media server with it.
  • A VOICE_STATE_UPDATE for the bot's own user with channel_id: null means a moderator disconnected it or the channel was deleted. Close the media connection; there is no need to send op 4.
  • The end-to-end encryption requirement of a channel does not apply to bots.
  • Only the first two voice updates per second take effect immediately; later ones can be delayed or merged.

Voice describes placement, permissions and capacity in full, and Voice State Update lists every field.

Limits

Limit Value
Identify with the same token 20 per minute; further attempts are held and retried by the gateway
Identify or Resume from one IP address 300 per minute; further attempts are ignored
Events you send on one connection 600 per minute; exceeding it closes with 4008
Presence updates (op 3) 5 per 20 seconds; extra updates are dropped
Inbound frame size 4096 bytes

See Gateway limits and rate limits for details.

Next: respond to messages.