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.
{
"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 |
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).
- 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 Identifypresencefield accepts the same fields, see Initial presence object. - A Unicode emoji goes in
emoji_nameand 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. Anemoji_idthat names no emoji rejects the whole custom status, and the previous one stays. expires_atis an ISO 8601 time after which the custom status is cleared.- A bot whose status is
invisibleappears 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.
- Wait for the guild's
GUILD_CREATE. Itsvoice_statesshow who is in which voice channel. -
Send Voice State Update (op 4) without a
connection_id, which opens a new connection. Send IDs as strings. -
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_UPDATEwithtoken,endpoint,channel_idandconnection_id. Keepconnection_id. Every session that can see the channel receives aVOICE_STATE_UPDATEfor the bot. - Connect to
endpointwithtokenusing 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 withsuppress: trueand nobody hears it. -
To leave, send op 4 with
channel_id: nulland theconnection_id, then disconnect from the media server. Withoutconnection_idthe leave is refused.
Things a voice bot must handle:
- A refused join (missing permission, full channel, timed-out bot) produces no event. Wait for
VOICE_SERVER_UPDATEwith a timeout, for example 15 seconds. - A later
VOICE_SERVER_UPDATEfor the same guild, after a region change or a move, carries a newtoken. Reconnect to the media server with it. - A
VOICE_STATE_UPDATEfor the bot's own user withchannel_id: nullmeans 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.