Quickstart: build a bot
Create a bot, post a message as the bot, then run a small Node.js program that answers in the thread whenever someone mentions it.
You need:
- A DonutChat account and a group chat where you’re the owner or a manager.
- curl, and Node.js 22 or later for the bot program.
Create the bot
Section titled “Create the bot”- Go to Profile > Tools & content > My Bots (desktop app: Settings > App > My bots).
- Select + (desktop app: Add a bot).
- Select Developer options > Build an API bot. In the web app, + opens the Create bot form directly, so skip this step.
- Enter a Name of 32 bytes or fewer, such as
Quickstart Bot. A longer name can’t post messages in the chat without a shorter sender name. - Enter a Username that no one else uses, such as
yourname_bot. - Under Trigger mode, choose When mentioned. The desktop app has no trigger mode setting and uses When mentioned.
- Select Create and continue (Android: Continue; web app: Create).
- Copy the token that the app shows. It starts with
donutbot_and is shown only once.
Create and manage bots has the exact steps for each app. Anyone who has the token can act as your bot, so keep it secret. The commands on this page read it from an environment variable:
export DONUT_BOT_TOKEN=donutbot_YOUR_TOKEN_HEREAdd the bot to a chat
Section titled “Add the bot to a chat”- Open the group chat.
- Open Chat info.
- Select Manage chat > Bots.
- Select +, or Add Bot if the chat has no bots yet.
- Select Add next to your bot.
The desktop app uses a different path. See Add a bot to a chat.
Find the chat ID
Section titled “Find the chat ID”List the chats the bot belongs to:
curl https://api.donutchat.com/bots/v1/chats \ -H "Authorization: Bearer $DONUT_BOT_TOKEN"{ "ok": true, "chats": [ { "chat_id": 1234, "chat_name": "Release team", "chat_type": "group", "is_active": true, "member_count": 6 } ], "total": 1}Copy the chat_id value exactly as printed. On iPhone, Android and the web app, you can also copy it from Chat info > Manage chat > Chat ID.
Send a message
Section titled “Send a message”Replace 1234 with your chat ID:
curl -X POST https://api.donutchat.com/bots/v1/messages \ -H "Authorization: Bearer $DONUT_BOT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"chat_id": 1234, "content": "Hello from my bot"}'{ "ok": true, "message_id": 5678, "chat_id": 1234}The message appears in the chat under your bot’s name. If you get 403 not_in_chat, check the chat ID and that the bot is in the chat. Send messages covers Markdown, sender names and the other options.
Reply when someone mentions the bot
Section titled “Reply when someone mentions the bot”This program connects to the event stream. When someone mentions the bot in a message or a thread reply, it answers in that thread with a thread reply. Save it as bot.mjs:
// bot.mjs: replies in the thread whenever someone mentions the bot.// Run with Node.js 22 or later: DONUT_BOT_TOKEN=donutbot_YOUR_TOKEN_HERE node bot.mjsconst TOKEN = process.env.DONUT_BOT_TOKEN;if (!TOKEN) throw new Error('Set DONUT_BOT_TOKEN to your bot token.');const API = 'https://api.donutchat.com/bots/v1';const STREAM = 'wss://api.donutchat.com/bots/v1/stream';
// IDs can be larger than Number.MAX_SAFE_INTEGER. Read large integers as BigInt// and write BigInt values back as plain JSON numbers.const parseJSON = (text) => JSON.parse(text, (key, value, context) => Number.isInteger(value) && !Number.isSafeInteger(value) ? BigInt(context.source) : value);const toJSON = (value) => JSON.stringify(value, (key, v) => (typeof v === 'bigint' ? JSON.rawJSON(v.toString()) : v));
async function post(path, body) { const res = await fetch(API + path, { method: 'POST', headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' }, body: toJSON(body), }); const text = await res.text(); if (!res.ok) throw new Error(`POST ${path} failed (${res.status}): ${text}`); return parseJSON(text);}
async function handle({ type, data }) { if ((type !== 'message.new' && type !== 'thread.reply.new') || !data.mentions_bot) return; await post('/thread-replies', { chat_id: data.chat_id, // A new message starts a thread; a thread reply continues its parent's thread. parent_message_id: type === 'message.new' ? data.message_id : data.parent_message_id, content: `Hi ${data.sender.name}! You said: ${data.text}`, });}
function connect() { // The event stream also accepts the token as a query parameter, which works with any WebSocket client. const ws = new WebSocket(`${STREAM}?token=${encodeURIComponent(TOKEN)}`); // DonutChat closes the connection if the client sends no text or binary message for // 5 minutes. Answering pings doesn't count, so send a small text frame every minute. const keepalive = setInterval(() => { if (ws.readyState === WebSocket.OPEN) ws.send('keepalive'); }, 60_000);
ws.addEventListener('message', ({ data }) => { const event = parseJSON(data); if (event.type === 'connected') console.log('Connected to the event stream.'); handle(event).catch((err) => console.error(err.message)); }); ws.addEventListener('close', ({ code }) => { clearInterval(keepalive); console.log(`Disconnected (code ${code}). Reconnecting in 5 seconds.`); setTimeout(connect, 5_000); });}
connect();Run it:
node bot.mjsWhen it prints Connected to the event stream., send a message in the chat that mentions the bot, such as @yourname_bot hello. The bot answers in that message’s thread. To keep the conversation going, reply in the thread and mention the bot again: in When mentioned mode, the bot receives only thread replies that mention it.
How the program works
Section titled “How the program works”- IDs:
parseJSONreads IDs that are too large for JavaScript numbers asBigInt, andtoJSONwrites them back as JSON numbers.JSON.parse()orresponse.json()would round them. See IDs are 64-bit integers. - Mentions: each
message.newandthread.reply.newevent says inmentions_botwhether it mentions the bot. Bots never receive messages from bots, including their own replies, so the bot can’t answer itself. - Threads: a new message’s
message_idbecomes theparent_message_idof the reply. A thread reply event already carries its thread’sparent_message_id. - Connection: DonutChat closes the connection if the client sends no text or binary message for 5 minutes; answering pings doesn’t count. The program sends a small text frame every minute, which DonutChat ignores, and reconnects whenever the connection closes.
If the bot doesn’t answer
Section titled “If the bot doesn’t answer”- It never prints
Connected to the event stream.Check the token, and check that the bot is active in My Bots. - It’s connected but stays silent. Check that your message mentions the bot’s username and that the bot is in the chat. Messages sent while the program isn’t connected usually aren’t delivered later.
- It prints
POST /thread-replies failed. The error code in the message says why. See Errors.
Next steps
Section titled “Next steps”- Thread replies and streaming: show a long answer while it’s being written.
- Event stream: every event type, trigger modes and reconnecting.
- Interactive cards: let people answer with buttons.
- Authentication: keep the token safe and regenerate it.