Everything you need to build a disdroc bot: authentication, every REST endpoint, the Socket.IO real-time protocol, rich embeds, and rate limits. Prefer a ready-made bot with no code? Check the Bot Marketplace instead. Building with an LLM? Paste bot-api-llms.txt into the chat first.
Creating an application requires a second factor on your disdroc account (not the bot) — either a verified email address or an authenticator app (TOTP). Almost every account already has this via its verified email, so most people never see a prompt for it; if you do get a "two-factor authentication required" error, verify your email or enable TOTP in Settings → Security.
dbot_xxxxxxxx…. If you lose it, regenerate it from the app's Token tab (the old one stops working immediately)./bot-invite/<appId>) and open it while logged into any server you manage — this adds the bot as a normal member with the member role. Give it more permissions the same way you would a person (server Roles settings).Authorization: Bot <token>, and start reading/sending with the endpoints below.A bot is an ordinary user account (is_bot flag set) with no password — it can never log in through /login. Every bot-facing endpoint checks a bearer-style header instead:
Authorization: Bot dbot_your_token_here
There is no OAuth flow and no scopes in v1 — a bot can do exactly what its assigned server role permits, same as a human member (view/send messages per channel, nothing more unless you grant it a role with more permissions).
Base URL: https://disdroc.com/api. All of these require the Authorization: Bot … header above.
| Method & path | What it does |
|---|---|
| GET /bot/v1/me | Your bot's own identity: id, username, displayName, tag, avatarPath. |
| GET /bot/v1/servers | Servers the bot is currently a member of. |
| GET /bot/v1/servers/:id | One server's details + the channels the bot can view. |
| GET /bot/v1/channels | Every chat the bot can see — DMs, groups, and server channels — flattened into one list. Use this at startup to know what to chat:join over the socket. |
| GET /bot/v1/channels/:id/messages | Recent history for one channel. Query params: before (ms epoch, default now), limit (1–100, default 50). |
| POST /bot/v1/channels/:id/messages | Send one message. Body: { "content": "text", "embed": {...} } — content alone, embed alone, or both. No attachments/replies/polls here — use the Socket.IO path (below) for those. |
| POST /bot/v1/voice/:channelId/join | Join an accessible server voice channel as an audio-only participant. |
| POST /bot/v1/voice/:channelId/play | Play one public HTTP(S) audio/video URL while joined. Body: { "url": "https://…" } (or audioUrl). Starting another source replaces the current one. |
| POST /bot/v1/voice/:channelId/stop | Stop playback but remain in the channel. |
| POST /bot/v1/voice/:channelId/leave | Leave voice and close every peer connection. |
Voice bots publish audio only: they do not receive human audio, video, or screen shares. The source URL must use port 80 or 443 and resolve to a public address. A server without the optional voice-bot WebRTC component returns 503 { "error": "voice_bot_unavailable" }.
member role obeys the
same rules a person with that role would.
A bot can manage members and moderate exactly as far as its own assigned server role allows — grant it Manage Roles, Kick Members, Ban Members, and/or Timeout Members in that server's Roles settings the same way you would for a person, then these become available:
| Method & path | What it does |
|---|---|
| GET /bot/v1/servers/:id/members | Every member, with their assigned role_ids. |
| GET /bot/v1/servers/:id/roles | Every role in the server, plus myPerms — the bot's own effective permission bitfield. |
| GET /bot/v1/servers/:id/members/:uid/perms | A specific member's permissions — check this for the person who typed a command before acting on their behalf, so your bot can't be puppeted into moderating someone the invoking human has no authority over. |
| PUT /bot/v1/servers/:id/members/:uid/roles/:roleId | Add one role to a member. Requires Manage Roles and that the role sits below the bot's own highest role. |
| DELETE /bot/v1/servers/:id/members/:uid/roles/:roleId | Remove one role from a member. Same requirements as above. |
| DELETE /bot/v1/servers/:id/members/:uid | Kick a member. Requires Kick Members. Body: { "reason": "..." } (optional). |
| POST /bot/v1/servers/:id/members/:uid/ban | Ban a member. Requires Ban Members. Body: { "reason": "...", "minutes": 0 } — both optional; minutes omitted or 0 is a permanent ban, otherwise it auto-lifts after that many minutes (capped at 1 year). |
| GET /bot/v1/servers/:id/bans | List current bans. Requires Ban Members. |
| DELETE /bot/v1/servers/:id/bans/:uid | Unban. Requires Ban Members. |
| POST /bot/v1/servers/:id/members/:uid/timeout | Timeout (temporary mute) a member. Requires Timeout Members. Body: { "minutes": 10, "reason": "..." } — minutes: 0 clears an existing timeout. |
All of the mutating routes above share a per-bot rate limit (30 calls/minute) separate from the general message-send limit, so a leaked token can't be used to mass-purge a server.
Your bot also gets a member:joined Socket.IO event (see below) whenever someone new joins a server it's in — handy for auto-role or welcome-message bots.
Register your own /name commands so they show up in the composer's /-autocomplete
for anyone in a server your bot is a member of — no need to tell your users to memorize a prefix. Kept
deliberately simple: one command name + description, and your bot gets the raw remainder of the message as a
single argument string (no typed/structured options).
| Method & path | What it does |
|---|---|
| GET /bot/v1/commands | List this bot's registered commands. |
| PUT /bot/v1/commands/:name | Create or update a command. :name must match ^[a-z0-9_-]{1,20}$. Body: { "description": "Roll dice, e.g. /roll 2d6" }. Capped at 25 commands per bot. |
| DELETE /bot/v1/commands/:name | Remove a command. |
When a member sends a message starting with /yourcommand in a server your bot belongs to, the
message posts normally (it's just text, same as any other message) and your bot receives a
bot:command Socket.IO event: { command, args, channelId, userId, messageId } — reply
the same way you always would, with message:send or the REST send route.
An embed is a small disdroc rich card: an optional title, description, image, and accent color. Pass it on POST /bot/v1/channels/:id/messages or over the socket's message:send — it renders in every client the same way.
{
"content": "",
"embed": {
"title": "Deploy finished",
"description": "Build #482 shipped in 41s.",
"imageUrl": "https://your-host.example/screenshot.png",
"color": "#57f287"
}
}
imageUrl must be an http(s) URL you host — disdroc just embeds it, it doesn't accept file uploads over the bot REST API. color is a 6-digit hex string; omit it to use the app's default accent. A message needs at least one of content, an attachment, or an embed field — it can't be completely empty.
For a bot that reacts to messages as they happen (instead of polling), connect over Socket.IO the same way the web client does — same gateway, same events, full validation pipeline (attachments, replies, polls included, unlike the REST shortcut above).
const { io } = require('socket.io-client');
const TOKEN = 'dbot_your_token_here';
const socket = io('https://disdroc.com', { auth: { token: TOKEN } });
let selfId = null;
socket.on('connect', async () => {
const me = await fetch('https://disdroc.com/api/bot/v1/me', {
headers: { Authorization: 'Bot ' + TOKEN },
}).then(r => r.json());
selfId = me.id;
const res = await fetch('https://disdroc.com/api/bot/v1/channels', {
headers: { Authorization: 'Bot ' + TOKEN },
}).then(r => r.json());
for (const c of res.channels) socket.emit('chat:join', c.id);
});
socket.on('message:new', (msg) => {
if (msg.user_id === selfId) return; // ignore your own messages to avoid loops
if (msg.content === '!ping') {
socket.emit('message:send', { chatId: msg.chat_id, content: 'pong' });
}
});
Key events: chat:join (emit, join a chat room), message:send (emit, {chatId, content, embed?} with an ack callback), message:new (listen, fires for every message in a joined chat — note it does not carry an is_bot flag, compare msg.user_id against your own id from /bot/v1/me instead), member:joined (listen, {serverId, user, joinedAt}, fires when anyone joins a server your bot is in), bot:command (listen, {command, args, channelId, userId, messageId}, fires when someone uses one of your registered slash commands).
Don't want to run your own server for a bot? Community Bot Hosting lets you paste your bot's token and your own script, and we run it for you — sandboxed. An admin reviews new scripts (and any edit to one) before they go live, since a hosted script runs unattended with real access to your bot.
The sandbox gives your code exactly one thing: an api object. There is no filesystem or network
access of your own in either runtime — every api.* call is proxied to the exact same
permission-gated REST endpoints documented above, so a hosted script can never do more than the bot's assigned
role allows.
Pick a runtime when you submit a script:
require, no process, no timers (setTimeout doesn't exist here).
api is an ambient global, NOT a function parameter — do not add it to your own signature.Promise chains work normally and
require works for a small set of safe core modules (assert, buffer,
crypto, events, path, querystring,
string_decoder, url, util, zlib) — fs,
net, http(s), child_process, and similar aren't available (there's no
filesystem or network to reach anyway; the process runs fully sandboxed with no network of its own).
api works exactly the same way as isolated-vm — an ambient identifier inside your script, not a
function parameter — so the same script body works under either runtime.// Hosted scripts export onMessage / onMemberJoin (schedule is not supported
// here — hosted scripts are event-driven only). `api` is an ambient global,
// NOT a function parameter — do not add it to your own signature.
module.exports.onMessage = async function(text, m, config) {
if (text.trim() === '!ping') await api.reply(m.chat_id, 'pong');
};
module.exports.onMemberJoin = async function(payload, config) {
await api.addRole(payload.serverId, payload.user.id, YOUR_ROLE_ID);
};
Available on api: reply(chatId, content), listMembers(serverId),
listRoles(serverId), memberPerms(serverId, uid),
addRole(serverId, uid, roleId), removeRole(serverId, uid, roleId),
kick(serverId, uid, reason), ban(serverId, uid, reason),
timeout(serverId, uid, minutes, reason) — same names and arguments as the REST endpoints above,
just called as plain async functions.
The config argument is whatever the server admin who installed your bot filled
in on its setup screen — a channel to post in, a mod role, a toggle, a custom prefix, whatever your bot needs
per server. You define that screen yourself: open your script's card in
Community Bot Hosting and click Setup Screen to add fields (text,
number, on/off, dropdown, or a channel/role picker). Anyone who then adds your bot from the Bot Marketplace
gets a form built from those fields, and the values they save arrive as config.yourFieldKey —
e.g. a field with key logChannel shows up as config.logChannel. A field left unset
(or a bot with no setup screen at all) just comes through as its declared default, or missing from
config if it has none.
Limits: 50KB of code per script, 32MB memory and a short per-call timeout enforced by the sandbox, and up to 10 consecutive errors before a script is auto-disabled (fix it and re-enable — no re-review needed unless you also changed the code). Prefer running it yourself instead of having us host it? The "disdroc Bot Builder" desktop tool does the same preset/script-picking on your own machine.
limit is capped at 100 per request — page with before.npm install socket.io-client node-fetch
const { io } = require('socket.io-client');
const TOKEN = 'dbot_your_token_here';
const BASE = 'https://disdroc.com';
const socket = io(BASE, { auth: { token: TOKEN } });
let selfId = null;
socket.on('connect', async () => {
console.log('connected as bot');
const me = await fetch(BASE + '/api/bot/v1/me', { headers: { Authorization: 'Bot ' + TOKEN } }).then(r => r.json());
selfId = me.id;
const { channels } = await fetch(BASE + '/api/bot/v1/channels', {
headers: { Authorization: 'Bot ' + TOKEN },
}).then(r => r.json());
channels.forEach(c => socket.emit('chat:join', c.id));
});
socket.on('message:new', (msg) => {
if (msg.user_id === selfId) return;
if (msg.content?.startsWith('!echo ')) {
socket.emit('message:send', { chatId: msg.chat_id, content: msg.content.slice(6) });
}
});
Run it with node bot.js — it stays connected and replies to !echo hello with hello in any channel it's a member of.