Build a bot for Translocate
A bot is a program that connects to Translocate, listens for events, and acts through a REST API. This page covers all of it.
Overview
There are two parts to the platform:
- The REST API at
https://translocate.app/api/v1. Your bot uses it to read and to act: post a message, react, register commands, answer a command. - The gateway, a Socket.IO namespace at
https://translocate.app/bot. Your bot stays connected to it and receives events as they happen.
You manage bots in the console. You need a Translocate account with a confirmed email address.
Concepts
Application. The thing you own and manage in the console. It has a name, a description, an icon and a token. You can have up to 10.
Bot account. Each application comes with one account that people see in member lists and on messages, tagged as a bot. It has no password and can't sign in to the app. Its handle looks like helper.bot.a1b2c3.
Token. The one credential for a bot. It starts with tlb_. It is shown once, when you create the application or regenerate the token. Translocate stores only a hash of it.
Install. Adding a bot to a space. Someone with the Manage bots permission opens your install link, picks a space and approves the permissions. They can only grant permissions they hold. A private application can only be added by you. A public one can be added by anyone with Manage bots. A space can hold up to 25 bots.
Managed role. Each install creates one role named after the bot, at the bottom of the role list. It holds the permissions from the install link. It can't be deleted, given to anyone else, or taken from the bot. To change it, someone with Manage bots uses Server Settings, Bots.
A bot starts with only what its role grants. Members get a baseline of permissions in every channel. Bots don't. A bot whose role lacks View Channel sees no channels and receives no messages. So your install link has to ask for View Channel. The default link asks for View Channel, Send Messages and Add Reactions.
Private channels. Having View Channel on the role is not enough for a private channel. Someone has to add the bot's role to that channel, the same way they would for any other role.
Spaces. A space is what the REST API calls a server. Paths and fields say server, such as serverId.
Quickstart
These steps get a bot online that answers !ping and has two slash commands, /echo and /slow.
- In the console, choose New application, name it, and copy the token that appears.
- On the application's page, under Install link, keep the default permissions and create the link. Open it and pick a space you manage.
- Run one of the bots below with your token in the
BOT_TOKENenvironment variable. - In that space, type
!ping, or type/to see your commands.
Node
Needs Node 18 or later.
mkdir my-bot && cd my-bot
npm init -y
npm install socket.io-client
Save this as bot.mjs:
import { io } from "socket.io-client";
const BASE = process.env.TRANSLOCATE_URL || "https://translocate.app";
const TOKEN = process.env.BOT_TOKEN;
const COMMANDS = [
{
name: "echo",
description: "Repeat what you say",
options: [
{ name: "text", description: "What to repeat", type: "string", required: true },
{ name: "private", description: "Only show it to you", type: "boolean" },
],
},
{ name: "slow", description: "Take a moment, then answer" },
];
async function api(method, path, body) {
const res = await fetch(`${BASE}/api/v1${path}`, {
method,
headers: { authorization: `Bot ${TOKEN}`, "content-type": "application/json" },
...(body === undefined ? {} : { body: JSON.stringify(body) }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(`${method} ${path}: ${res.status} ${data.error || ""}`);
return data;
}
const socket = io(`${BASE}/bot`, { auth: { token: TOKEN }, transports: ["websocket"] });
let me = null;
socket.on("ready", async (ready) => {
me = ready.user;
console.log(`Connected as ${me.displayName || me.username}, in ${ready.servers.length} space(s)`);
await api("PUT", "/commands", { commands: COMMANDS });
});
socket.on("connect_error", (e) => console.log(`Can't connect: ${e.message}`));
socket.on("messageCreate", async (message) => {
if (message.author.bot || message.author.id === me?.id) return; // never answer a bot
if (message.content === "!ping") {
await api("POST", `/channels/${message.channelId}/messages`, { content: "pong", replyToId: message.id });
}
});
socket.on("interactionCreate", async (i) => {
const option = (name) => i.options.find((o) => o.name === name)?.value;
if (i.command.name === "echo") {
// Answer within 3 seconds, or the person sees a failure.
await api("POST", `/interactions/${i.id}/callback`, {
token: i.token, type: "reply", content: option("text"), ephemeral: option("private") === true,
});
} else if (i.command.name === "slow") {
await api("POST", `/interactions/${i.id}/callback`, { token: i.token, type: "defer" });
await new Promise((r) => setTimeout(r, 2000)); // up to 15 minutes is fine after a defer
await api("POST", `/interactions/${i.id}/followups`, { token: i.token, content: "Done." });
}
});
BOT_TOKEN=tlb_xxx node bot.mjs
The reference bot does the same thing and is what Translocate's own tests run: bot.mjs and package.json.
Python
Needs Python 3.9 or later, python-socketio and requests.
pip install "python-socketio[client]" requests
Save this as bot.py:
import os
import time
import requests
import socketio
BASE = os.environ.get("TRANSLOCATE_URL", "https://translocate.app")
TOKEN = os.environ["BOT_TOKEN"]
NS = "/bot"
COMMANDS = [
{
"name": "echo",
"description": "Repeat what you say",
"options": [
{"name": "text", "description": "What to repeat", "type": "string", "required": True},
{"name": "private", "description": "Only show it to you", "type": "boolean"},
],
},
{"name": "slow", "description": "Take a moment, then answer"},
]
http = requests.Session()
http.headers["Authorization"] = f"Bot {TOKEN}"
def api(method, path, body=None):
res = http.request(method, f"{BASE}/api/v1{path}", json=body, timeout=10)
if not res.ok:
raise RuntimeError(f"{method} {path}: {res.status_code} {res.text}")
return res.json()
sio = socketio.Client()
me = {}
@sio.on("ready", namespace=NS)
def on_ready(ready):
me.update(ready["user"])
name = me.get("displayName") or me["username"]
print(f"Connected as {name}, in {len(ready['servers'])} space(s)")
api("PUT", "/commands", {"commands": COMMANDS})
@sio.on("connect_error", namespace=NS)
def on_connect_error(data):
print(f"Can't connect: {data}")
@sio.on("messageCreate", namespace=NS)
def on_message(message):
author = message["author"]
if author.get("bot") or author["id"] == me.get("id"):
return # never answer a bot
if message["content"] == "!ping":
api("POST", f"/channels/{message['channelId']}/messages",
{"content": "pong", "replyToId": message["id"]})
@sio.on("interactionCreate", namespace=NS)
def on_interaction(i):
options = {o["name"]: o["value"] for o in i["options"]}
if i["command"]["name"] == "echo":
# Answer within 3 seconds, or the person sees a failure.
api("POST", f"/interactions/{i['id']}/callback", {
"token": i["token"], "type": "reply",
"content": options["text"], "ephemeral": options.get("private") is True,
})
elif i["command"]["name"] == "slow":
api("POST", f"/interactions/{i['id']}/callback", {"token": i["token"], "type": "defer"})
time.sleep(2) # up to 15 minutes is fine after a defer
api("POST", f"/interactions/{i['id']}/followups", {"token": i["token"], "content": "Done."})
sio.connect(BASE, namespaces=[NS], auth={"token": TOKEN}, transports=["websocket"])
sio.wait()
BOT_TOKEN=tlb_xxx python bot.py
Authentication
REST calls carry the token in a header:
Authorization: Bot tlb_your_token_here
The gateway takes it in the Socket.IO handshake auth object, as { token }. A token in a URL or query string is never accepted, on either one.
A bot token only works on /api/v1 and the gateway. It can't reach the API people use, and a person's session can't reach /api/v1.
Calls fail like this when the token can't be used:
| Status | Code | Meaning |
|---|---|---|
| 401 | BOT_TOKEN_INVALID | The token is wrong, or it was regenerated. |
| 403 | BOT_DISABLED | Translocate staff disabled this bot. |
| 403 | BOT_OWNER_UNAVAILABLE | The owner's account is banned, deactivated or under 16. |
Permissions
The install link carries a permission bitfield as a decimal string. Bits go beyond 32 bits, so use a big integer type and never a plain JavaScript number with | or &.
https://translocate.app/app?bot=<applicationId>&perms=57344
The console builds this link for you. These are the permissions that change what a bot can do today:
| Permission | Value | Effect on a bot |
|---|---|---|
| View Channel | 8192 | See a channel, read it, and receive its events. Needed for almost everything. |
| Send Messages | 16384 | Post messages and public command replies. |
| Add Reactions | 32768 | Add and remove its own reactions. |
| Format Code | 2097152 | Without it, code blocks and inline code in its messages are unwrapped to plain text. |
| Colored Text | 4194304 | Without it, color tags in its messages are removed. |
| Use External Emojis | 67108864 | Without it, custom emojis from other spaces become plain text. |
| Mention @everyone | 32 | Without it, @everyone and @here don't ping anyone. |
| Bypass AutoMod | 2147483648 | Its messages skip AutoMod rules. |
| Administrator | 1 | Every permission, in every channel. Avoid it. A leaked token then exposes the whole space. |
The API has no moderation or management endpoints yet, so permissions like Kick Members or Manage Channels do nothing for a bot today. Don't ask for them.
Rate limits
Limits are per application, so running on several hosts doesn't add to them. The one exception is the per-IP limit below.
| Limit | Applies to |
|---|---|
| 300 requests per minute | Every /api/v1 call by this bot. |
| 60 writes per minute | Every write: messages, edits, deletes, reactions, command registration, interaction callbacks and followups. |
| 5 messages per 5 seconds | Each channel or DM, when posting with POST /channels/:id/messages or POST /dms/:id/messages. |
| 1200 requests per minute | Per IP address, before the token is checked. |
| 20 followups | Per interaction. |
| 5 connections | Open gateway connections per bot. |
Over a limit, you get status 429 and a Retry-After header in seconds. Wait that long, then retry. The followup limit is the exception: it has no Retry-After, because it doesn't reset.
HTTP/1.1 429 Too Many Requests
Retry-After: 4
{ "error": "Too many requests — please slow down and try again shortly." }
REST reference
Base URL: https://translocate.app/api/v1. Send and receive JSON, with Content-Type: application/json on any request with a body. Errors look like { "error": "message", "code": "OPTIONAL_CODE" }.
A 404 means the thing doesn't exist or your bot can't see it. The two look the same on purpose, so a bot learns nothing about spaces it isn't in. If your bot gets a 404 on a channel you know exists, check that its role has View Channel there.
Objects
User
The public profile only. Email, date of birth, status and other private fields are never included.
{
"id": "cmh3k2x0a0001",
"username": "ana",
"displayName": "Ana",
"avatarColor": "#14b8a6",
"avatarUrl": null,
"customStatus": null,
"nameColor": null,
"nameFont": null,
"createdAt": "2026-10-01T12:00:00.000Z"
}
Bots also carry "bot": true. Translocate staff carry "staff": true. Display fields can be missing on nested authors, so treat everything except id and username as optional.
Message
{
"id": "cmh3k9p2b0007",
"type": "text",
"content": "!ping",
"channelId": "cmh3k5a1c0003",
"serverId": "cmh3k4z9f0002",
"author": { "id": "cmh3k2x0a0001", "username": "ana", "displayName": "Ana" },
"createdAt": "2026-11-04T09:30:00.000Z",
"editedAt": null,
"attachments": [],
"reactions": [{ "emoji": "👍", "count": 2, "users": ["cmh3k2x0a0001", "cmh3k2x0a0009"] }],
"replyTo": null,
"pinned": false,
"embed": null
}
- A message in a thread also has
parentChannelId. - A DM has
dmChannelIdinstead ofchannelIdandserverId. replyToisnullor{ id, author, preview }, wherepreviewis the first 120 characters.attachmentsitems haveid,url,filename,contentType,size,width,heightandspoiler.typecan be something other thantext, such aspollorsticker. Those cards reach a bot without their card data.- Live location cards are never delivered, and location previews arrive with
embedset tonull.
Channel
{
"id": "cmh3k5a1c0003",
"name": "general",
"type": "text",
"topic": null,
"serverId": "cmh3k4z9f0002",
"categoryId": null,
"parentId": null,
"position": 0,
"adult": false
}
Server (space)
{ "id": "cmh3k4z9f0002", "name": "The Rift", "iconUrl": null, "adult": false, "memberCount": 42 }
Member
{
"id": "cmh3k6b2d0004",
"user": { "id": "cmh3k2x0a0001", "username": "ana" },
"nickname": null,
"roleIds": ["cmh3k7c3e0005"],
"joinedAt": "2026-10-02T08:00:00.000Z"
}
Self
/users/@meYour bot's account and application.
{ "user": { "id": "...", "username": "helper.bot.a1b2c3", "displayName": "Helper", "bot": true },
"application": { "id": "...", "name": "Helper", "description": null, "isPublic": false } }
Spaces and members
/serversThe spaces your bot is in. Returns { servers: [Server] }.
/servers/:idOne space. Returns { server }.
/servers/:id/channelsThe channels your bot can see, in order. Returns { channels: [Channel] }. Threads aren't listed. Map channels and dividers are never returned.
/servers/:id/membersNeeds View Channel on the bot's role, or 403. Returns { members: [Member], next }.
| Query | Meaning |
|---|---|
limit | 1 to 100. Default 50. |
after | A member id. Pass the next value from the last page. next is null on the last page. |
/servers/:id/members/:userIdOne member, by user id. Needs View Channel. Returns { member }.
Channels and messages
/channels/:idReturns { channel }.
/channels/:id/messagesRecent messages, oldest first. Returns { messages: [Message] }.
| Query | Meaning |
|---|---|
limit | 1 to 100. Default 50. |
before | A message id. Returns messages older than it. |
/channels/:id/messagesPost a text message. Needs View Channel and Send Messages. Returns { message }.
| Field | Type | Meaning |
|---|---|---|
content | string | Required, not blank, up to 50,000 characters. |
replyToId | string | Optional. A message in the same channel. Ignored otherwise. |
nonce | string | Optional, 1 to 64 characters. If a request is retried with the same nonce in the same channel, you get the original message back rather than a duplicate. |
Bots post text only. Any other field is ignored, so attachments, polls and stickers can't be sent.
curl https://translocate.app/api/v1/channels/CHANNEL_ID/messages \
-H "Authorization: Bot $BOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"content":"Hello from curl"}'
/messages/:idEdit your bot's own message. Body: { content }. Returns { message }. Someone else's message gives 403.
/messages/:idDelete your bot's own message. Returns { ok: true }. Someone else's message gives 403, even with Manage Messages.
Reactions
/messages/:id/reactions/:emoji/messages/:id/reactions/:emojiAdd or remove your bot's reaction. Both need Add Reactions, and both are safe to repeat. :emoji is a URL-encoded emoji, or a custom emoji token like <:name:id>, up to 128 characters. Returns { reactions: [{ emoji, count, users }] }.
PUT /api/v1/messages/MESSAGE_ID/reactions/%F0%9F%91%8D
DMs
See DM rules. Group DMs aren't available to bots.
/dmsYour bot's own conversations. Returns { channels: [{ id, recipient }] }, where recipient is a User.
/dms/:id/messagesThe latest messages, oldest first. limit is 1 to 100, default 50. There is no paging further back. Returns { messages: [Message] }.
/dms/:id/messagesReply in a conversation. Body: { content }. Returns { message }, or 403 with DM_REPLY_ONLY.
Commands
/commands/commands/servers/:id/commands/servers/:id/commandsRead or replace your global commands, or the commands for one space. PUT takes { commands: [...] } and returns the saved list as { commands: [{ id, name, description, options, serverId }] }. See Slash commands.
Interactions
/interactions/:id/callbackThe first answer to a command. It must arrive within 3 seconds.
| Field | Type | Meaning |
|---|---|---|
token | string | Required. The interaction token from the event. It goes in the body, never the URL. |
type | string | Required. reply or defer. |
content | string | The reply text. Required for reply. |
ephemeral | boolean | Only the person who ran the command sees it. Also sets the default for followups. |
Returns { message } for a public reply, { ok: true, ephemeral: true } for an ephemeral reply, and { ok: true } for a defer.
/interactions/:id/followupsA further message, for up to 15 minutes after the command ran. Body: { token, content, ephemeral }. ephemeral is optional and defaults to what the callback chose. Returns the same as a reply. Up to 20 per interaction.
Gateway
The gateway is Socket.IO version 4, namespace /bot, on https://translocate.app. Use a Socket.IO client. A plain WebSocket won't work.
// Node
const socket = io("https://translocate.app/bot", { auth: { token }, transports: ["websocket"] });
# Python
sio.connect("https://translocate.app", namespaces=["/bot"], auth={"token": token}, transports=["websocket"])
The gateway only sends. Your bot acts through REST, and anything it emits on the socket is ignored.
You receive events for channels your bot can see at that moment. If it loses View Channel, events stop within moments. If it gains access, they start.
A bot can hold up to 5 connections. Every connection receives every event, so extra connections duplicate work rather than share it. Run one unless you have a reason not to.
Connecting and reconnecting
- Each successful connection gets a fresh
readyevent. There is no resume. - Events missed while offline are not replayed. If you need them, read history with
GET /channels/:id/messagesafterready. - Do your setup in the
readyhandler, because it runs again after every reconnect. - Socket.IO reconnects by itself after a network drop or a Translocate deploy. It does not reconnect when the server ended the connection on purpose. That happens when the token is regenerated, or the bot or its owner is disabled, and the disconnect reason is
io server disconnect. Restart with a valid token.
A refused connection raises connect_error, and its message is one of these:
| Message | Meaning |
|---|---|
BOT_TOKEN_INVALID | The token is wrong or was regenerated. |
BOT_DISABLED | Translocate staff disabled this bot. |
BOT_OWNER_UNAVAILABLE | The owner's account can't be used. |
Too many connections | This bot already has 5 connections open. |
Unauthorized | Something failed on the server. Try again. |
In Python, a refused first connection also raises socketio.exceptions.ConnectionError from sio.connect().
Events
| Event | When |
|---|---|
ready | The connection is set up. |
messageCreate | A message was posted in a channel your bot can see. |
messageUpdate | A message changed. |
messageDelete | A message was deleted. |
reactionAdd, reactionRemove | Someone added or removed a reaction. |
channelCreate, channelDelete, channelsUpdate, threadCreate | Channels changed. |
memberJoin, memberLeave, memberUpdate | Membership changed in a space. |
serverJoin, serverLeave | Your bot was added to or removed from a space. |
dmCreate, dmUpdate | A DM with your bot was sent or changed. |
interactionCreate | Someone ran one of your commands. |
Typing, presence, voice and location events are never sent.
ready
{
"user": { "id": "...", "username": "helper.bot.a1b2c3", "displayName": "Helper", "bot": true },
"application": { "id": "...", "name": "Helper" },
"servers": [{ "id": "...", "name": "The Rift", "iconUrl": null, "adult": false }]
}
messageCreate, messageUpdate
The payload is a Message. You also receive your bot's own messages, so check the author. messageUpdate fires for edits and for other changes to a message, such as a link preview arriving, a pin, or a new reply in its thread.
messageDelete
{ "id": "cmh3k9p2b0007", "channelId": "cmh3k5a1c0003" }
reactionAdd, reactionRemove
{ "channelId": "...", "messageId": "...", "userId": "...", "emoji": "👍" }
Channel events
// channelCreate
{ "serverId": "...", "channel": { /* Channel */ } }
// channelDelete
{ "serverId": "...", "channelId": "..." }
// threadCreate
{ "serverId": "...", "parentChannelId": "...", "channel": { /* Channel */ } }
// channelsUpdate
{ "serverId": "..." }
channelsUpdate says something about this space's channels or your access changed: a rename, a move, a role change. It doesn't say what. Fetch GET /servers/:id/channels if you keep your own list.
Member events
Sent for every space your bot is in.
// memberJoin
{ "serverId": "...", "member": { "user": { /* User */ }, "nickname": null, "roleIds": [] } }
// memberLeave
{ "serverId": "...", "userId": "..." }
// memberUpdate
{ "serverId": "...", "userId": "...", "nickname": "Ana B" }
serverJoin, serverLeave
// serverJoin
{ "serverId": "...", "server": { "id": "...", "name": "My space", "iconUrl": null, "adult": false } }
// serverLeave
{ "serverId": "..." }
serverLeave means your bot was removed, kicked or banned, or the space was deleted. After it, calls for that space return 404.
dmCreate, dmUpdate
The payload is a Message with dmChannelId. dmCreate also fires for your bot's own replies, so check the author or it will answer itself.
interactionCreate
{
"id": "6f1c1f0e-2f7b-4c1e-9a53-0d1f3f6b9a11",
"token": "pS0v...",
"type": "command",
"command": { "id": "cmh3kc1a00011", "name": "pick" },
"serverId": "...",
"channelId": "...",
"user": { /* User, the person who ran it */ },
"member": { "nickname": null, "roleIds": ["..."] },
"options": [
{ "name": "who", "type": "user", "value": "cmh3k2x0a0001" },
{ "name": "where", "type": "channel", "value": "cmh3k5a1c0003" },
{ "name": "n", "type": "integer", "value": 2 }
],
"resolved": {
"users": { "cmh3k2x0a0001": { "id": "cmh3k2x0a0001", "username": "ana", "nickname": null } },
"channels": { "cmh3k5a1c0003": { "id": "cmh3k5a1c0003", "name": "general", "type": "text" } }
},
"respondBy": 1793786400000,
"followupsUntil": 1793787297000
}
options holds only what the person filled in, in the order you defined. resolved has the user and channel objects for those options. respondBy and followupsUntil are Unix times in milliseconds.
Slash commands
Registering
PUT replaces the whole list for that scope, so send every command each time. An empty array removes them all. Commands that keep their name keep their id. It is safe to register on every ready.
PUT /api/v1/commands
{
"commands": [
{
"name": "roll",
"description": "Roll a die",
"options": [
{
"name": "sides",
"description": "How many sides",
"type": "integer",
"required": true,
"choices": [{ "name": "Six", "value": 6 }, { "name": "Twenty", "value": 20 }]
},
{ "name": "secret", "description": "Only show it to you", "type": "boolean" }
]
}
]
}
Rules
| Item | Rule |
|---|---|
| Commands | Up to 50 per scope. No repeated names in a list. |
| Command and option names | 1 to 32 lowercase letters, digits, - or _. |
| Descriptions | Required. Cut to 100 characters. |
| Options | Up to 10 per command. No repeated names. Required options come before optional ones. |
| Choices | 1 to 25, on string, integer and number options only. Each has a name up to 100 characters and a value of the option's type. A string value is up to 100 characters. |
| Values at run time | A string value is up to 2000 characters. Values are checked against your definition before your bot hears about them. |
Option types
| Type | value you receive |
|---|---|
string | Text. |
integer | A whole number. |
number | Any number. |
boolean | true or false. |
user | A user id for someone in this space. The object is in resolved.users, with nickname. |
channel | A channel id. Both the person and your bot can see it. The object is in resolved.channels. |
Global or per space
Global commands, from PUT /commands, appear in every space your bot is in. Commands from PUT /servers/:id/commands appear in that space only, and are removed if the bot leaves it. If both define the same name, that space's version wins there.
A person sees a command in a channel when they hold Use Slash Commands there and your bot can see the channel.
Answering
Everything in an interaction was checked by the server, so you can trust who ran it. You have 3 seconds to call POST /interactions/:id/callback. There are two ways to answer:
replysends the answer now. After it you can still send followups.defershows the person that your bot is thinking. You then have up to 15 minutes to send a followup. The first followup ends the thinking state. A second callback gets409, so after a defer always use followups.
Ephemeral replies go only to the person who ran the command. They aren't stored, so they don't appear in history, search or notifications, and they vanish when that person reloads. They pass through the same AutoMod and timeout checks as a normal message. Both public and ephemeral replies need Send Messages.
{ "token": "pS0v...", "type": "defer", "ephemeral": true }
When things go wrong
| What happened | What the person sees | What your bot gets |
|---|---|---|
| Your bot isn't connected | "Helper is offline right now." (BOT_OFFLINE, status 503) | Nothing. The command never reaches you. |
| No callback in 3 seconds | "Helper didn't respond in time." | A late call gets 410, INTERACTION_EXPIRED. |
| A reply or followup is refused, for example no Send Messages, a timeout, or AutoMod | "Helper couldn't reply: ..." with the reason. | The error for that call. The interaction is over, and later calls get 410. |
| Deferred but no followup in 15 minutes | "Helper never finished." | A late call gets 410. |
| Your bot was removed or its token regenerated mid-command | "Helper is no longer available." | Later calls get 410. |
| Translocate restarted | The command is lost, and they have to run it again. | Interactions are held in memory, so calls get 404, INTERACTION_UNKNOWN. |
DM rules
A bot never starts a conversation. It can only reply, and only when all of this is true:
- The person wrote to it first in that conversation.
- The two still share a space.
- Neither has blocked the other.
- The other side is a person, not a bot.
Otherwise POST /dms/:id/messages returns 403 with DM_REPLY_ONLY. Your bot can only see its own conversations. Bots can't be added as friends or to group DMs.
Error codes
Not every error has a code. Check the status first, and show or log error.
| Status | Meaning |
|---|---|
| 400 | The request is invalid. error says why. |
| 401 | The token is missing or wrong. |
| 403 | The bot is known but isn't allowed to do this. |
| 404 | It doesn't exist, or the bot can't see it. |
| 409 | It conflicts with the current state. |
| 410 | The interaction is over. |
| 429 | Rate limited. See Retry-After. |
| 503 | Translocate is having trouble. Try again. |
| Code | Status | Meaning |
|---|---|---|
BOT_TOKEN_INVALID | 401 | The token is wrong or was regenerated. |
BOT_DISABLED | 403 | Staff disabled this bot. |
BOT_OWNER_UNAVAILABLE | 403 | The owner's account can't be used. |
TIMED_OUT | 403 | The bot is timed out in this space. The body also has until, reason and byName. |
AUTOMOD | 403 | An AutoMod rule blocked the message. |
ADULT | 403 | 18+ restriction. Also what an install gets when the bot can't join an 18+ space. |
DM_REPLY_ONLY | 403 | See DM rules. |
INTERACTION_UNKNOWN | 404 | Wrong id, wrong token, or not your bot's interaction. |
INTERACTION_ACKED | 409 | You already called the callback. Use followups. |
INTERACTION_NOT_ACKED | 409 | A followup came before the callback. |
INTERACTION_EXPIRED | 410 | Too late, or the interaction already failed. |
People who add or run your bot can meet these, which you may hear about from them:
| Code | Status | Meaning |
|---|---|---|
BOT_OFFLINE | 503 | The bot isn't connected to the gateway. |
BOT_UNAVAILABLE | 403 | The bot is disabled, or its owner's account can't be used. |
BOT_NO_ACCESS | 403 | The bot can't see this channel. |
CANT_GRANT | 403 | The installer doesn't hold a permission the link asks for. |
BANNED | 403 | The bot is banned from that space. |
Security
Protect the token
- Keep it on your server, in an environment variable or a secret store. Never put it in a URL, in client code, in a mobile app, or in a public repository.
- Tokens start with
tlb_so secret scanners can find them. - If a token leaks, regenerate it in the console. The old one stops working at once, and open connections are cut.
Treat user text as untrusted
- Ignore messages from bots, including your own. Check
author.bot. Two bots answering each other is how loops start. - If your bot has Mention @everyone and repeats user text, anyone can use it to ping the whole space. Don't ask for that permission on a bot that echoes, or strip
@everyoneand@herefirst. - Ask for the fewest permissions you need. Everything your bot can do, a thief with its token can do.
What a bot can't get
- Location. Map channels are closed to bots, live location cards aren't delivered, and location previews are removed.
- Voice and calls, typing and presence.
- Other people's DMs.
- Private account fields such as email and date of birth.
The 18+ rule
A bot is treated as under 18 unless its owner is a verified adult. An owner with no date of birth on file counts as not verified. Such a bot can't join an 18+ space, can't see 18+ channels, and receives no events from them. If the owner's standing changes, or a space turns 18+ later, the bot loses access.
Bots follow the rules
Timeouts, AutoMod, bans and channel permissions apply to bots the same way they apply to people. Bots must follow the Terms. Staff can disable a bot, and a banned owner takes their bots down with them.