Translocate developers
On this page

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.

  1. In the console, choose New application, name it, and copy the token that appears.
  2. On the application's page, under Install link, keep the default permissions and create the link. Open it and pick a space you manage.
  3. Run one of the bots below with your token in the BOT_TOKEN environment variable.
  4. 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:

StatusCodeMeaning
401BOT_TOKEN_INVALIDThe token is wrong, or it was regenerated.
403BOT_DISABLEDTranslocate staff disabled this bot.
403BOT_OWNER_UNAVAILABLEThe 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:

PermissionValueEffect on a bot
View Channel8192See a channel, read it, and receive its events. Needed for almost everything.
Send Messages16384Post messages and public command replies.
Add Reactions32768Add and remove its own reactions.
Format Code2097152Without it, code blocks and inline code in its messages are unwrapped to plain text.
Colored Text4194304Without it, color tags in its messages are removed.
Use External Emojis67108864Without it, custom emojis from other spaces become plain text.
Mention @everyone32Without it, @everyone and @here don't ping anyone.
Bypass AutoMod2147483648Its messages skip AutoMod rules.
Administrator1Every 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.

LimitApplies to
300 requests per minuteEvery /api/v1 call by this bot.
60 writes per minuteEvery write: messages, edits, deletes, reactions, command registration, interaction callbacks and followups.
5 messages per 5 secondsEach channel or DM, when posting with POST /channels/:id/messages or POST /dms/:id/messages.
1200 requests per minutePer IP address, before the token is checked.
20 followupsPer interaction.
5 connectionsOpen 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 dmChannelId instead of channelId and serverId.
  • replyTo is null or { id, author, preview }, where preview is the first 120 characters.
  • attachments items have id, url, filename, contentType, size, width, height and spoiler.
  • type can be something other than text, such as poll or sticker. Those cards reach a bot without their card data.
  • Live location cards are never delivered, and location previews arrive with embed set to null.

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

GET/users/@me

Your 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

GET/servers

The spaces your bot is in. Returns { servers: [Server] }.

GET/servers/:id

One space. Returns { server }.

GET/servers/:id/channels

The channels your bot can see, in order. Returns { channels: [Channel] }. Threads aren't listed. Map channels and dividers are never returned.

GET/servers/:id/members

Needs View Channel on the bot's role, or 403. Returns { members: [Member], next }.

QueryMeaning
limit1 to 100. Default 50.
afterA member id. Pass the next value from the last page. next is null on the last page.
GET/servers/:id/members/:userId

One member, by user id. Needs View Channel. Returns { member }.

Channels and messages

GET/channels/:id

Returns { channel }.

GET/channels/:id/messages

Recent messages, oldest first. Returns { messages: [Message] }.

QueryMeaning
limit1 to 100. Default 50.
beforeA message id. Returns messages older than it.
POST/channels/:id/messages

Post a text message. Needs View Channel and Send Messages. Returns { message }.

FieldTypeMeaning
contentstringRequired, not blank, up to 50,000 characters.
replyToIdstringOptional. A message in the same channel. Ignored otherwise.
noncestringOptional, 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"}'
PATCH/messages/:id

Edit your bot's own message. Body: { content }. Returns { message }. Someone else's message gives 403.

DELETE/messages/:id

Delete your bot's own message. Returns { ok: true }. Someone else's message gives 403, even with Manage Messages.

Reactions

PUT/messages/:id/reactions/:emoji
DELETE/messages/:id/reactions/:emoji

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

GET/dms

Your bot's own conversations. Returns { channels: [{ id, recipient }] }, where recipient is a User.

GET/dms/:id/messages

The latest messages, oldest first. limit is 1 to 100, default 50. There is no paging further back. Returns { messages: [Message] }.

POST/dms/:id/messages

Reply in a conversation. Body: { content }. Returns { message }, or 403 with DM_REPLY_ONLY.

Commands

GET/commands
PUT/commands
GET/servers/:id/commands
PUT/servers/:id/commands

Read 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

POST/interactions/:id/callback

The first answer to a command. It must arrive within 3 seconds.

FieldTypeMeaning
tokenstringRequired. The interaction token from the event. It goes in the body, never the URL.
typestringRequired. reply or defer.
contentstringThe reply text. Required for reply.
ephemeralbooleanOnly 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.

POST/interactions/:id/followups

A 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 ready event. There is no resume.
  • Events missed while offline are not replayed. If you need them, read history with GET /channels/:id/messages after ready.
  • Do your setup in the ready handler, 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:

MessageMeaning
BOT_TOKEN_INVALIDThe token is wrong or was regenerated.
BOT_DISABLEDTranslocate staff disabled this bot.
BOT_OWNER_UNAVAILABLEThe owner's account can't be used.
Too many connectionsThis bot already has 5 connections open.
UnauthorizedSomething failed on the server. Try again.

In Python, a refused first connection also raises socketio.exceptions.ConnectionError from sio.connect().

Events

EventWhen
readyThe connection is set up.
messageCreateA message was posted in a channel your bot can see.
messageUpdateA message changed.
messageDeleteA message was deleted.
reactionAdd, reactionRemoveSomeone added or removed a reaction.
channelCreate, channelDelete, channelsUpdate, threadCreateChannels changed.
memberJoin, memberLeave, memberUpdateMembership changed in a space.
serverJoin, serverLeaveYour bot was added to or removed from a space.
dmCreate, dmUpdateA DM with your bot was sent or changed.
interactionCreateSomeone 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

ItemRule
CommandsUp to 50 per scope. No repeated names in a list.
Command and option names1 to 32 lowercase letters, digits, - or _.
DescriptionsRequired. Cut to 100 characters.
OptionsUp to 10 per command. No repeated names. Required options come before optional ones.
Choices1 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 timeA string value is up to 2000 characters. Values are checked against your definition before your bot hears about them.

Option types

Typevalue you receive
stringText.
integerA whole number.
numberAny number.
booleantrue or false.
userA user id for someone in this space. The object is in resolved.users, with nickname.
channelA 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:

  • reply sends the answer now. After it you can still send followups.
  • defer shows 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 gets 409, 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 happenedWhat the person seesWhat 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 restartedThe 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.

StatusMeaning
400The request is invalid. error says why.
401The token is missing or wrong.
403The bot is known but isn't allowed to do this.
404It doesn't exist, or the bot can't see it.
409It conflicts with the current state.
410The interaction is over.
429Rate limited. See Retry-After.
503Translocate is having trouble. Try again.
CodeStatusMeaning
BOT_TOKEN_INVALID401The token is wrong or was regenerated.
BOT_DISABLED403Staff disabled this bot.
BOT_OWNER_UNAVAILABLE403The owner's account can't be used.
TIMED_OUT403The bot is timed out in this space. The body also has until, reason and byName.
AUTOMOD403An AutoMod rule blocked the message.
ADULT40318+ restriction. Also what an install gets when the bot can't join an 18+ space.
DM_REPLY_ONLY403See DM rules.
INTERACTION_UNKNOWN404Wrong id, wrong token, or not your bot's interaction.
INTERACTION_ACKED409You already called the callback. Use followups.
INTERACTION_NOT_ACKED409A followup came before the callback.
INTERACTION_EXPIRED410Too late, or the interaction already failed.

People who add or run your bot can meet these, which you may hear about from them:

CodeStatusMeaning
BOT_OFFLINE503The bot isn't connected to the gateway.
BOT_UNAVAILABLE403The bot is disabled, or its owner's account can't be used.
BOT_NO_ACCESS403The bot can't see this channel.
CANT_GRANT403The installer doesn't hold a permission the link asks for.
BANNED403The 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 @everyone and @here first.
  • 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.