Skip to content
donutchat.com

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.
  1. Go to Profile > Tools & content > My Bots (desktop app: Settings > App > My bots).
  2. Select + (desktop app: Add a bot).
  3. Select Developer options > Build an API bot. In the web app, + opens the Create bot form directly, so skip this step.
  4. 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.
  5. Enter a Username that no one else uses, such as yourname_bot.
  6. Under Trigger mode, choose When mentioned. The desktop app has no trigger mode setting and uses When mentioned.
  7. Select Create and continue (Android: Continue; web app: Create).
  8. 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:

Terminal window
export DONUT_BOT_TOKEN=donutbot_YOUR_TOKEN_HERE
  1. Open the group chat.
  2. Open Chat info.
  3. Select Manage chat > Bots.
  4. Select +, or Add Bot if the chat has no bots yet.
  5. Select Add next to your bot.

The desktop app uses a different path. See Add a bot to a chat.

List the chats the bot belongs to:

Terminal window
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.

Replace 1234 with your chat ID:

Terminal window
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.

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.mjs
const 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:

Terminal window
node bot.mjs

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

  • IDs: parseJSON reads IDs that are too large for JavaScript numbers as BigInt, and toJSON writes them back as JSON numbers. JSON.parse() or response.json() would round them. See IDs are 64-bit integers.
  • Mentions: each message.new and thread.reply.new event says in mentions_bot whether 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_id becomes the parent_message_id of the reply. A thread reply event already carries its thread’s parent_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.
  • 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.