Bot Receive (WebSocket)
The Bot Receive API is a WebSocket endpoint that streams chat events (new messages, reactions, membership changes) to your bot in real time. Combine it with Bot Send to build bots that react to messages.
At a glance
Section titled “At a glance”| Direction | Chat → bot |
| Auth | Authorization: Bearer vifbot_<token> header or ?token=<token> query param |
| Transport | WebSocket over HTTPS |
| Typical use case | AI assistants, event-driven bots, anything that needs to see user messages in real time |
When to use this
Section titled “When to use this”Pick Bot Receive when your bot needs to read chat activity. Pair it with Bot Send to reply. If your bot only posts (no reading), Bot Send alone is enough. If you only need to post into one specific chat without a bot identity, use an Incoming Webhook.
For users
Section titled “For users”A bot only receives events for chats it has been added to. Add the bot to every chat it should be able to see.
Bots also have a trigger mode that filters message events server-side:
mention(default): the bot only receivesmessage.newwhen its@usernameappears in the message.all: the bot receives everymessage.newin the chat.manual: the bot receives nomessage.newevents (reactions and membership events still flow).
Trigger mode is configured via the bot’s detail screen or the management API. It has no effect on outbound messages sent with Bot Send.
For developers
Section titled “For developers”Endpoint
Section titled “Endpoint”GET wss://api.donutchat.com/bots/v1/streamAuthenticate with the bot’s bearer token, either as an Authorization header or — for WebSocket clients that can’t set headers — as a ?token=<token> query parameter.
Connection flow
Section titled “Connection flow”- Client dials
wss://api.donutchat.com/bots/v1/streamwith the token. - Server validates the token. Auth/availability failures return an HTTP JSON error before the WebSocket upgrade (see Pre-upgrade errors).
- Server upgrades to WebSocket and sends a
connectedcontrol frame. - Server streams events as they occur in chats the bot has been added to.
- Client replies to WebSocket ping frames (handled automatically by most WS libraries) to keep the connection alive.
- On disconnect, the server cleans up the subscription. Reconnect to resume; see Reconnecting and replay.
Event envelope
Section titled “Event envelope”All messages from the server (except control frames) share this envelope:
{ "event_id": "evt_1710000001_abc123", "type": "message.new", "timestamp": "2026-04-20T12:00:00Z", "data": { /* type-specific fields */ }}Event types
Section titled “Event types”message.new
Section titled “message.new”{ "event_id": "evt_1710000001_abc123", "type": "message.new", "timestamp": "2026-04-20T12:00:00Z", "data": { "message_id": 12345, "chat_id": 678, "chat_name": "Engineering Team", "chat_type": "group", "sender": { "id": 42, "name": "Alice Kim", "username": "alice" }, "text": "Hey @mybot, what's the weather today?", "mentions_bot": true }}chat_typeis one ofdirect,group,feed,self.mentions_botistruewhen the bot’s@usernameappears as a whole-word mention intext.- Reply/thread linkage is not included in the event payload, and the bot API does not currently expose a way to fetch the referenced message. A future event-payload addition may include
reply_to_message_id.
reaction.add and reaction.remove
Section titled “reaction.add and reaction.remove”{ "event_id": "evt_1710000002_def456", "type": "reaction.add", "timestamp": "2026-04-20T12:00:01Z", "data": { "message_id": 12345, "chat_id": 678, "emoji": "👍", "reactor": { "id": 43, "name": "Bob Lee" } }}reaction.remove has the same shape. Reaction events are always delivered regardless of trigger mode.
Control frames
Section titled “Control frames”The server emits two categories of control frame:
Envelope-free (no event_id, not replayed on reconnect):
| Type | Data | Meaning |
|---|---|---|
connected | {"bot_id": <int>} | Handshake complete; the stream is live. |
rate_limited | {"retry_after_ms": 5000} | Event rate limit hit; in-flight events are dropped until the bucket refills. retry_after_ms is a constant 5000 today. |
Enveloped (include event_id and timestamp, participate in replay):
| Type | Data | Meaning |
|---|---|---|
chat_added | {"chat_id": <int>, "chat_name": "<string>"} | Bot was added to a new chat while connected; subsequent events for that chat will now flow. |
chat_removed | {"chat_id": <int>} | Bot was removed from a chat while connected; no further events from that chat. |
Checkpoint event_id from membership frames the same way as message.new / reaction.* — otherwise a reconnect with last_event_id will replay the membership change you already processed.
Pre-upgrade errors
Section titled “Pre-upgrade errors”Returned as an HTTP response (not a WebSocket frame) with body shape {ok, error, message}:
| HTTP status | error code | Meaning |
|---|---|---|
| 401 | unauthorized | Token is missing, invalid, or the bot has been disabled |
| 405 | method_not_allowed | Used a method other than GET |
| 500 | internal_error | Server failed to load the bot’s chat list |
| 503 | stream_unavailable | Server was started without event streaming enabled |
Samples
Section titled “Samples”import { Tabs, TabItem } from ‘@astrojs/starlight/components’;
# websocat is a small CLI WebSocket client — install with `cargo install websocat` or via your package manager.websocat \ -H "Authorization: Bearer vifbot_YOUR_TOKEN_HERE" \ wss://api.donutchat.com/bots/v1/streamimport WebSocket from 'ws';
const TOKEN = process.env.VIFBOT_TOKEN;let lastEventId = null;
function connect() { const url = lastEventId ? `wss://api.donutchat.com/bots/v1/stream?last_event_id=${lastEventId}` : 'wss://api.donutchat.com/bots/v1/stream';
const ws = new WebSocket(url, { headers: { Authorization: `Bearer ${TOKEN}` }, });
ws.on('message', (data) => { const frame = JSON.parse(data.toString()); if (frame.event_id) { lastEventId = frame.event_id; } if (frame.type === 'message.new') { console.log(`[${frame.data.chat_name}] ${frame.data.sender.name}: ${frame.data.text}`); } });
ws.on('close', () => { console.log('Disconnected; reconnecting in 2s...'); setTimeout(connect, 2000); });
ws.on('error', (err) => { console.error('WebSocket error:', err.message); });}
connect();Reconnecting and replay
Section titled “Reconnecting and replay”When a client reconnects, it can supply the last event id it successfully processed as a query parameter:
wss://api.donutchat.com/bots/v1/stream?last_event_id=evt_1710000001_abc123The server replays any buffered events newer than that id before emitting the connected control frame, then resumes live delivery. The replay buffer holds up to 5 minutes or 100 events per bot, whichever is smaller — a longer disconnect means some events are permanently lost.
Always persist event_id from the most recent event you processed so reconnects can catch up. The Node.js sample above does this.
Limits and gotchas
Section titled “Limits and gotchas”- Rate limit: 100 events/minute per bot. When exceeded, the server emits a
rate_limitedcontrol frame withretry_after_ms: 5000and drops in-flight events until the token bucket refills. - Replay window: 5 minutes / 100 events per bot. Longer disconnects lose events.
- Trigger mode filters server-side.
mentionmode means your bot’smessage.newfeed only contains messages that@mentionthe bot.manualmode means nomessage.newevents reach the bot (reactions and membership events still flow). - No reply/thread linkage in events. If a
message.newevent is a reply, fetch the referenced message via the REST API — the event payload does not include it. - One connection per bot. Opening a second connection for the same bot closes the existing one. Use reconnect-with-replay to recover from transient disconnects.
Related
Section titled “Related”- Bot Send — reply to events by posting messages under the same bot identity.
- Incoming Webhooks — simple one-way posting, no event stream.