DiyorDevelopers

Diyor bot API

Bots are members of your servers that are driven by your code. They read and send messages, react, answer slash commands and buttons, and receive events over one WebSocket. Everything is free and has no usage quota beyond the rate limits below. Base URL: https://app.diyor.chat/api/v1

Quick start

  1. Open the developer portal, sign in with your Diyor account and create an app. Every app has a bot user.
  2. Copy the bot token (shown once; you can reset it). Keep it secret: anyone with it controls the bot.
  3. Choose the permissions your bot needs and open the install link. A server admin (someone with Manage server) picks a server and approves. The admin who installs a bot is responsible for it in that server.
  4. Run your bot with an official library: JavaScript (Node.js 22+) or Python (3.10+).

JavaScript

npm install @diyorchat/bot
import { Client } from '@diyorchat/bot';

const client = new Client({ token: process.env.DIYOR_BOT_TOKEN });

client.on('messageCreate', async (message) => {
  if (message.author.id === client.user.id) return;
  if (message.content === '!ping') await message.reply('pong!');
});

client.command({ name: 'roll', description: 'Roll a die' }, (interaction) =>
  interaction.reply({
    embeds: [{ title: '🎲 Roll', description: `You rolled **${1 + Math.floor(Math.random() * 6)}**`, color: 0x5865f2 }],
  }));

const { user } = await client.login();
console.log(`Logged in as ${user.name}`);

Python

pip install diyorchat
import os, random
import diyorchat

client = diyorchat.Client(token=os.environ["DIYOR_BOT_TOKEN"])

@client.event
async def on_message_create(message):
    if message.author.id == client.user.id:
        return
    if message.content == "!ping":
        await message.reply("pong!")

@client.command(description="Roll a die")
async def roll(interaction):
    await interaction.reply(embeds=[{"title": "🎲 Roll", "description": f"You rolled **{random.randint(1, 6)}**", "color": 0x5865F2}])

client.run()

Reading every message (like !ping above) needs Message content access in the portal; without it the bot sees only messages that mention it (@BotName) and its own.

Libraries (JavaScript, Python)

Both libraries cover the same ground: one gateway connection with reconnects, events, slash commands, buttons, embeds, REST shortcuts, and stopping cleanly when Diyor disables the app. Anything else is a plain HTTP call: see the REST API and openapi.json.

JavaScript: @diyorchat/bot

ES module, no dependencies on Node.js 22+ (built-in fetch and WebSocket). On Node 18–21 install ws and pass it: new Client({ token, WebSocket }). TypeScript types are included.

APIDescription
new Client({ token, intents?, baseUrl? })intents default to all: messages, reactions, members, voice, dms
client.command(definition, handler)Register a slash command; all commands are written on login()
client.login()Writes commands, connects the gateway, resolves with READY data
client.on(event, listener)ready, serverCreate, serverDelete, channelsUpdate, messageCreate, messageUpdate, messageDelete, reactionsUpdate, memberAdd, memberRemove, voiceStateUpdate, directMessage, interactionCreate, error, debug
client.sendMessage(server, channel, options)options: a string or { content, embeds, components, reply_to }
client.sendDirectMessage(server, user, options)Direct message to a member of that server
client.fetchMessages(server, channel, { before, limit })Message history
client.serversMap of the servers the bot is in (channels, categories, permissions)
message.reply / edit / delete / react / unreactShortcuts on Message
interaction.reply / defer / update / followUpAnswer an interaction; reply({ ephemeral: true }) is visible only to the user
client.rest.get/post/patch/put/delete(path, body)Raw REST calls (429 is retried automatically)

Python: diyorchat

Python 3.10+, asyncio, one dependency (aiohttp), fully typed. Besides bots it includes OAuth2Client for Sign in with Diyor and verify_signature for an HTTPS endpoint.

APIDescription
diyorchat.Client(token, *, intents=None, base_url=...)Same intents as above
@client.event async def on_message_create(message)Or @client.on("message_create"); events use snake_case names: ready, server_create, message_create, direct_message, interaction_create, error, …
@client.command(description=..., options=[...])Slash command; the function name is the command name
client.run()Blocks; or await client.start() / client.close(). Raises LoginFailure or ApplicationDisabled
await client.wait_for(event, check=..., timeout=...)Wait for the next matching event
send_message / send_direct_message / fetch_messages / fetch_channels / fetch_membersREST shortcuts; client.rest for raw calls
message.reply / edit / delete / react / unreact
interaction.reply(ephemeral=True) / defer / update / follow_upcommand_name, options, custom_id

Authentication

Send the bot token on every REST call and on the gateway connection:

Authorization: Bot dvn_bot_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Bodies are JSON (Content-Type: application/json). Errors look like {"code": "unknown_channel", "message": "Unknown channel."}. A bot can only see the servers it was installed in, and only the members of those servers.

Gateway

One WebSocket carries the events of all servers the bot is in:

GET wss://app.diyor.chat/gateway?intents=messages,reactions,members,voice,dms
Authorization: Bot <token>
  • Every frame is JSON: {"op": "event", "t": "MESSAGE_CREATE", "d": {…}}.
  • Intents choose the event groups you receive (default: all). READY, SERVER_*, CHANNELS_UPDATE and INTERACTION_CREATE always arrive.
  • Heartbeat: send {"op":"ping"} every 30 seconds; the answer is {"op":"pong"}.
  • Reconnect with backoff (1 s, 2 s, 4 s … 30 s) when the socket closes. Events are not replayed: fetch history over REST if you need it.
  • You may open more than one connection (for example during a deploy); each receives all events.
  • A single-server connection is also available: /gateway?server={server} (its first event is READY with that server’s channels).

Events

EventIntentDataWhen
READY—{ user, servers: [{ id, name }] }Connected. Followed by SERVER_CREATE for each server.
SERVER_CREATE—{ id, name, channels, categories, permissions }A server is available (also when the bot is added to a new one).
SERVER_DELETE—{ id }The bot was removed from the server.
CHANNELS_UPDATE—{ server_id, channels, categories }Visible channels changed.
MESSAGE_CREATEmessagesMessageA message in a text channel.
MESSAGE_UPDATEmessagesMessageA message was edited (or its buttons/embeds changed).
MESSAGE_DELETEmessages{ id, server_id, channel_id }A message was deleted.
MESSAGE_REACTIONS_UPDATEreactions{ server_id, message_id, reactions }Reactions of a message (full list).
MEMBER_ADDmembers{ server_id, user: { id, name, bot } }Someone joined.
MEMBER_REMOVEmembers{ server_id, user_id }Someone left or was removed.
VOICE_STATE_UPDATEvoice{ server_id, user_id, channel_id, previous_channel_id? }Joined, left or moved between voice channels (channel_id null = left).
DM_MESSAGE_CREATEdmsMessage + dm_user_idA member wrote to the bot in a direct message.
INTERACTION_CREATE—InteractionA slash command or button press (when the app has no HTTPS endpoint).

Message content is empty unless the app has Message content access, the message mentions the bot, or the bot wrote it. Direct messages to the bot always include content. The same rule applies to attachments: attachments[].url is set only when the bot can see the content; download it with the bot token (Authorization: Bot …; Python await client.download_attachment(a), JavaScript await client.downloadAttachment(a)).

REST API

Paths are relative to https://app.diyor.chat/api/v1. Full schema: openapi.json (OpenAPI 3.1).

MethodPathDescription
GET/users/@meThe bot user.
GET/users/@me/serversServers the bot is in.
GET/servers/{server}/channelsChannels and categories the bot can see.
GET/servers/{server}/membersMembers (needs Server members access).
GET/servers/{server}/channels/{channel}/messagesNewest first. ?before={message}&limit=1-100 (default 50).
POST/servers/{server}/channels/{channel}/messagesSend: { content?, embeds?, components?, reply_to? }
PATCH/servers/{server}/channels/{channel}/messages/{message}Edit the bot’s own message: { content?, embeds?, components? }
DELETE/servers/{server}/channels/{channel}/messages/{message}Own messages, or any with manage_messages.
PUT/servers/{server}/channels/{channel}/messages/{message}/reactions/{emoji}Add a reaction (URL-encoded emoji).
DELETE/servers/{server}/channels/{channel}/messages/{message}/reactions/{emoji}Remove the bot’s reaction.
GET/servers/{server}/channels/{channel}/queueAnnouncement channel only: the queued announcement { id, content, author, embeds, components, queued_at, publish_at }, or 404 queue_empty.
POST/servers/{server}/channels/{channel}/queueQueue an announcement while the hourly limit is active: { content?, embeds?, components? }. 202 with the queue entry; 200 with the message if it could be published right away; 409 queue_full if one is already waiting (one per server).
PATCH/servers/{server}/channels/{channel}/queueEdit the queued announcement, whoever queued it: { content?, embeds?, components? }. The bot becomes its author.
DELETE/servers/{server}/channels/{channel}/queueRemove the queued announcement.
POST/servers/{server}/users/{user}/messagesDirect message to a member (needs send_direct_messages).
GET/servers/{server}/attachments/{attachment}Download a message attachment (the attachment’s url; same rules as message content).
GET/applications/@me/commandsThe app’s slash commands.
PUT/applications/@me/commandsReplace all slash commands.
POST/interactions/{id}/{token}/callbackAnswer an interaction (no bot token; the interaction token authenticates).
POST/webhooks/{id}/{token}Incoming webhook (no bot token). ?wait=true returns the message; multipart for files.
GET/webhooks/{id}/{token}The webhook itself (name, channel).
GET / PATCH / DELETE/webhooks/{id}/{token}/messages/{message}A message the webhook sent: read, edit (content, embeds) or delete.
POST/oauth2/tokenExchange an authorization code or refresh token (OAuth2 client).
POST/oauth2/revokeRevoke an OAuth2 token.
GET/users/@me (Bearer)With an OAuth2 access token: the signed-in user.
GET/users/@me/servers (Bearer)With an OAuth2 access token and the servers scope: the user’s servers.
GET/directoryPublic app directory (no token). ?search=&category=&page=
GET/directory/{id}A listed app with its commands (no token).
curl -X POST https://app.diyor.chat/api/v1/servers/$SERVER/channels/$CHANNEL/messages \
  -H "Authorization: Bot $DIYOR_BOT_TOKEN" -H "Content-Type: application/json" \
  -d '{"content": "Deploy finished", "embeds": [{"title": "v1.4.2", "color": "#2ecc71"}]}'

Objects

Message

{
  "id": "2b0d…",                  "server_id": "0ui4avbqf0ev",   "channel_id": "a1b2c3",
  "author": { "id": "…", "name": "Ayşe", "avatar_url": "https://app.diyor.chat/resim/h/…?v=…" },   // avatar_url: null for emoji/no picture; webhook: { …, webhook: true }; system: { id: "system", system: true }
  "content": "hello",             "timestamp": "2026-10-07T09:00:00.000Z",  "edited_timestamp": null,
  "reply_to": null,               "attachments": [{ "id": "9f1c…", "name": "photo.webp", "size": 52311, "content_type": "image/webp", "url": "https://app.diyor.chat/api/v1/servers/0ui4avbqf0ev/attachments/9f1c…" }],   // url: download with the bot token; null without content access
  "reactions": [{ "emoji": "👍", "count": 2 }],     "pinned": false,
  "components": [],               "embeds": [],
  "interaction": { "user_name": "Ayşe", "command": "roll" }   // or null
}

Channel

{ "id": "a1b2c3", "type": "text", "name": "general", "category_id": null, "position": 0, "can_send": true }
{ "id": "d4e5f6", "type": "voice", "name": "Lounge", "category_id": "k1", "position": 0, "can_connect": true, "user_limit": 0 }
{ "id": "k1", "type": "category", "name": "Talk", "position": 0 }

Embed (up to 3 per message)

{
  "title": "Build #128",                      // 256
  "description": "All **42** tests passed",   // 4000, Markdown
  "url": "https://ci.example.com/128",        // https only
  "color": 3066993,                           // or "#2ecc71"
  "fields": [{ "name": "Branch", "value": "main", "inline": true }],   // up to 10; name 256, value 1024
  "footer": { "text": "CI" },                 // 2048
  "image": { "url": "https://example.com/chart.png" }
}

All text in the embeds of one message: at most 6000 characters.

Button (up to 5 per message)

{ "type": "button", "label": "Snooze", "style": "primary", "custom_id": "snooze:42" }
{ "type": "button", "label": "Open", "style": "link", "url": "https://example.com" }
// style: primary | secondary (default) | success | danger | link;  custom_id: 1–100 chars;  "disabled": true

Slash commands

Commands belong to the app and appear in every server the bot is in when someone types /. Writing the list replaces all commands (up to 50, each with up to 10 options):

PUT /applications/@me/commands
[
  {
    "name": "remind",                  // 1–32: lowercase letters, digits, "_" or "-"
    "description": "Set a reminder",  // up to 100
    "options": [
      { "name": "minutes", "description": "In how many minutes", "type": "integer", "required": true },
      { "name": "text", "description": "What to remind", "type": "string" }
    ]
  }
]

Option types: string, integer, number, boolean, user (a member’s id). Required options come first. In Diyor the user types /remind 10 drink water; the last string option takes the rest of the line.

Interactions

A slash command or a button press creates an interaction:

{
  "id": "0ui4avbqf0ev-6f1c…", "token": "9c1e…", "type": "command",     // or "button"
  "application_id": "…", "server_id": "0ui4avbqf0ev", "channel_id": "a1b2c3",
  "user": { "id": "…", "name": "Ayşe", "avatar_url": null },
  "data": { "name": "remind", "options": { "minutes": 10, "text": "drink water" } }
  // button: "data": { "custom_id": "snooze:42", "message_id": "…" }
}

Answer within 3 seconds with a callback, or the user sees “did not respond”. The interaction token is valid for 15 minutes.

POST /interactions/{id}/{token}/callback        (no Authorization header)
{ "type": "message", "data": { "content": "Done!", "ephemeral": true, "embeds": [], "components": [] } }
{ "type": "defer" }                               // shows "Bot is thinking…"; send "message" within 15 minutes
{ "type": "update", "data": { "content": "Snoozed", "components": [] } }   // buttons only: edits that message
  • ephemeral messages are shown only to the user who ran the command and are not stored.
  • After the first answer you can send more message callbacks (follow-ups) within 15 minutes.
  • Messages answering a command show “Ayşe used /remind” above them.

HTTPS endpoint (serverless bots)

Set an Interactions endpoint URL in the portal and interactions are POSTed there instead of the gateway. Your response body is the callback ({ type, data }), sent within 3 seconds. The portal’s Test button sends {"type":"ping"}: answer {"type":"pong"}.

Every request is signed with your app’s signing secret. Verify it and reject requests older than 5 minutes:

X-Diyor-Timestamp: 1791364800
X-Diyor-Signature: sha256=<hex HMAC-SHA256(secret, "<timestamp>.<raw body>")>
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(secret, timestamp, signature, rawBody) {
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const expected = 'sha256=' + createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
  return signature?.length === expected.length && timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

// handler
if (!verify(SECRET, req.headers['x-diyor-timestamp'], req.headers['x-diyor-signature'], rawBody)) return res.status(401).end();
const i = JSON.parse(rawBody);
if (i.type === 'ping') return res.json({ type: 'pong' });
if (i.type === 'command' && i.data.name === 'roll') return res.json({ type: 'message', data: { content: '🎲 4' } });

In Python: diyorchat.verify_signature(secret, headers["X-Diyor-Timestamp"], headers["X-Diyor-Signature"], raw_body).

To send a later message from such a bot, use the callback URL with the same token (within 15 minutes) or the REST API with the bot token.

Direct messages

POST /servers/{server}/users/{user}/messages sends a direct message to a member of that server. The bot needs the send_direct_messages permission there, and people can turn off messages from bots in their settings (403 dm_closed). Bots cannot message other bots. Replies arrive as DM_MESSAGE_CREATE with dm_user_id.

Permissions & access

Installing gives the bot a managed role with the permissions it asked for; admins can change it like any role and channel overrides apply. The bot follows the same rules and rate limits as people. Permissions appear in SERVER_CREATE as:

PermissionAllows
view_channelsSee channels and read messages
send_messagesWrite in text channels, react
manage_messagesDelete and pin other people’s messages
connectSee who is in voice channels
send_direct_messagesSend direct messages to members of the server
send_announcementsPost in the server’s announcement channel (id "anons"). The channel accepts one message per hour from anyone: a second one gets 429 with retry_after (seconds). One announcement can wait in the queue (…/queue) and is published when the hour is up; anyone with this permission can edit or remove it
create_invitesCreate invite links
manage_channelsCreate, edit and delete channels
manage_rolesCreate roles, assign them
kick_membersRemove members
ban_membersBan members
moderate_voiceMute and move people in voice
manage_serverServer name, icon, invites, integrations
administratorEverything

Privileged access (developer portal, App settings):

  • Message content: content of every message the bot can see. Without it, only messages mentioning the bot and its own.
  • Server members: GET /servers/{server}/members.

Incoming webhooks

Server admins create webhooks in Server settings → Integrations for a text channel. Posting to the URL writes a message with the webhook’s name and avatar (marked BOT). No bot token needed; the URL is the secret.

POST https://app.diyor.chat/api/v1/webhooks/{id}/{token}
{ "content": "Nightly backup completed", "username": "Backup", "embeds": [{ "title": "42 GB", "color": "#2ecc71" }] }
-> 204 No Content
  • Slack format: {"text": "…"} is accepted on the same URL, also as payload= form data.
  • GitHub: use …/{token}/github as the payload URL (content type JSON): pushes, pull requests, issues, comments, reviews, releases, stars, forks and workflow runs.
  • Discord compatible: /api/webhooks/… and /api/v10/webhooks/… work too, so tools written for Discord webhooks can use the same URL with discord.com replaced. avatar_url (https) sets the picture for one message; ?wait=true responds with the created message instead of 204.
  • Files: send multipart/form-data with payload_json and files[0] … files[9]: up to 10 files per message and 100 MB per request. The size of each file, the daily upload quota and how long files are kept follow the server owner’s plan. attachment:// images in embeds are not supported (they are dropped).
  • Edit or delete: PATCH / DELETE …/{token}/messages/{message id}, only for messages the webhook sent. Deleting removes its files.
  • Limits: 2000 characters, JSON body up to 1 MB, 30 messages per minute per webhook.

Full guide with examples, every field and error: Incoming webhooks.

curl -F 'payload_json={"content":"Build #812"}' -F 'files[0]=@report.pdf' -F 'files[1]=@screenshot.png' \
  "https://app.diyor.chat/api/webhooks/{id}/{token}?wait=true"

Sign in with Diyor (OAuth2)

Let people sign in to your website or app with their Diyor account (OAuth 2.0 authorization code flow, RFC 6749, with PKCE). In the portal, add your redirect URIs (https, or http://localhost while developing). Your app id is the client_id.

  • Confidential client (a website with a server): create a client secret and send it on every token request. PKCE is still recommended.
  • Public client (single-page, desktop or mobile app that cannot keep a secret): do not create a secret; every authorization request must use PKCE and token requests send only client_id.

1. Send the user to Diyor

https://diyor.chat/oauth2/authorize
  ?client_id=<app id>
  &redirect_uri=https://example.com/auth/diyor/callback   // exactly as registered
  &response_type=code
  &scope=identify email servers
  &state=<random, stored in the user's session>
  &code_challenge=<BASE64URL(SHA256(verifier))>&code_challenge_method=S256   // PKCE (required for public clients)

The user signs in if needed and sees your app, its verification status and the requested scopes. If they approve, Diyor redirects to redirect_uri?code=…&state=…; if not, to redirect_uri?error=access_denied&state=…. Always compare state with the value stored in that browser’s session. If the user already approved the same scopes, the consent screen is skipped (add prompt=consent to always show it). Malformed requests (bad scope, missing PKCE method, state over 1000 characters) are redirected back with error=… for verified apps; for unverified apps Diyor shows the error itself.

ScopeGives
identifyid, name, avatar_url (always included)
emailemail (Diyor does not verify email addresses: email_verified is false; identify users by id)
serversThe servers the user is a member of: id, name, icon_url, owner

2. Exchange the code (server side, within 10 minutes, once)

POST https://app.diyor.chat/api/v1/oauth2/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)      // or client_id / client_secret in the body

grant_type=authorization_code&code=…&redirect_uri=…&code_verifier=…

-> { "access_token": "dvn_oa_…", "token_type": "Bearer", "expires_in": 604800,
     "refresh_token": "dvn_or_…", "scope": "identify email servers" }

Codes and refresh tokens are single use, even under concurrent requests. Access tokens last 7 days. Refresh with grant_type=refresh_token&refresh_token=… (plus the secret for confidential clients); every refresh returns a new refresh token and the old one stops working. Tokens never carry more than what the user currently allows. Resetting the Diyor password revokes every grant. Errors follow RFC 6749: {"error": "invalid_grant", "error_description": "…"}.

3. Call the API

GET https://app.diyor.chat/api/v1/users/@me
Authorization: Bearer dvn_oa_…
-> { "id": "e778f9cc-…", "name": "Ayşe", "bot": false, "avatar_url": "https://app.diyor.chat/resim/h/…", "email": "ayse@example.com", "email_verified": false }

GET https://app.diyor.chat/api/v1/users/@me/servers          // servers scope
-> [{ "id": "0ui4avbqf0ev", "name": "Design team", "icon_url": null, "owner": true }]

With Python, diyorchat.OAuth2Client(client_id, client_secret=..., redirect_uri=...) builds the authorization URL with state and PKCE (authorization_url()) and wraps exchange_code, refresh, revoke, fetch_user and fetch_servers.

Access tokens can only call these two endpoints; they never act for the user in Diyor. POST /oauth2/revoke (token + client credentials) signs the user out; revoking a refresh token revokes the whole grant. Users see and remove apps in Settings › Account › Authorized apps. A complete example without dependencies: ornekler/diyor-ile-giris in the Diyor repository.

App directory & verification

The app directory lists apps the Diyor team reviewed; people find them there and add them to a server in one click. In the portal, fill in the App directory section (category, a longer introduction, website, privacy policy, terms and the permissions to request) and press Submit for review. You need a bot avatar, a short description and a privacy policy URL, and Anyone can add must be on.

  • Approved apps are verified: a ✓ next to the BOT label everywhere in Diyor, on the install and consent screens. Renaming a verified or listed app removes the ✓ and sends it back to review.
  • Unverified bots can join at most 100 servers. Submit for review before you reach that (you can opt out of the directory and only ask for verification).
  • If an app is rejected, the reason is shown in the portal; fix it and submit again. Listing changes go live immediately, and Diyor can unlist an app.
  • Apps that break the rules can be disabled: the token stops working and live connections close with {"op":"close","d":{"reason":"application_disabled"}} and code 4010 (the library stops reconnecting).
GET https://app.diyor.chat/api/v1/directory?search=reminder&category=utility&page=0
-> { "applications": [{ "id", "name", "description", "avatar_url", "category", "verified", "server_count", "install_url" }],
     "total": 1, "page": 0, "page_size": 24,
     "categories": ["moderation", "fun", "utility", "games", "social", "productivity", "developer"] }

Limits & errors

LimitValue
REST requests50 per second per bot token (429 + Retry-After)
Messages and reactionsSame as a person: 30 messages and 60 reactions per minute in each server
Message content2000 characters
Embeds / buttons3 embeds (6000 characters total), 5 buttons per message
Commands50 per app, 10 options each
InteractionFirst answer within 3 seconds; token valid for 15 minutes
Servers per gateway connection500
Servers per unverified bot100 (verified bots: no limit)
OAuth2Code: 10 minutes, single use · access token: 7 days · 10 redirect URIs per app
StatusCodes
400invalid_json, empty_message, content_too_long, invalid_components, invalid_embeds, invalid_commands, invalid_emoji, invalid_intents, invalid_type, already_acknowledged, interaction_timeout
401unauthorized
403missing_permission, missing_access, cannot_edit, cannot_message_user, dm_closed
404not_found, unknown_server, unknown_channel, unknown_message, unknown_user, unknown_interaction, interaction_expired
429rate_limited

Manage your apps in the developer portal. Machine-readable API: openapi.json.