Skip to content

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.

DirectionChat → bot
AuthAuthorization: Bearer vifbot_<token> header or ?token=<token> query param
TransportWebSocket over HTTPS
Typical use caseAI assistants, event-driven bots, anything that needs to see user messages in real time

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.

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 receives message.new when its @username appears in the message.
  • all: the bot receives every message.new in the chat.
  • manual: the bot receives no message.new events (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.

GET wss://api.donutchat.com/bots/v1/stream

Authenticate 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.

  1. Client dials wss://api.donutchat.com/bots/v1/stream with the token.
  2. Server validates the token. Auth/availability failures return an HTTP JSON error before the WebSocket upgrade (see Pre-upgrade errors).
  3. Server upgrades to WebSocket and sends a connected control frame.
  4. Server streams events as they occur in chats the bot has been added to.
  5. Client replies to WebSocket ping frames (handled automatically by most WS libraries) to keep the connection alive.
  6. On disconnect, the server cleans up the subscription. Reconnect to resume; see Reconnecting and replay.

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_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_type is one of direct, group, feed, self.
  • mentions_bot is true when the bot’s @username appears as a whole-word mention in text.
  • 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.
{
"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.

The server emits two categories of control frame:

Envelope-free (no event_id, not replayed on reconnect):

TypeDataMeaning
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):

TypeDataMeaning
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.

Returned as an HTTP response (not a WebSocket frame) with body shape {ok, error, message}:

HTTP statuserror codeMeaning
401unauthorizedToken is missing, invalid, or the bot has been disabled
405method_not_allowedUsed a method other than GET
500internal_errorServer failed to load the bot’s chat list
503stream_unavailableServer was started without event streaming enabled

import { Tabs, TabItem } from ‘@astrojs/starlight/components’;

Terminal window
# 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/stream
import 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();

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_abc123

The 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.

  • Rate limit: 100 events/minute per bot. When exceeded, the server emits a rate_limited control frame with retry_after_ms: 5000 and 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. mention mode means your bot’s message.new feed only contains messages that @mention the bot. manual mode means no message.new events reach the bot (reactions and membership events still flow).
  • No reply/thread linkage in events. If a message.new event 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.
  • Bot Send — reply to events by posting messages under the same bot identity.
  • Incoming Webhooks — simple one-way posting, no event stream.