Что такое бот
Бот читает и пишет в текстовых каналах серверов, куда его добавили, ставит реакции, показывает кнопки, меню, карточки и формы, отвечает на команды через «/» и узнаёт о событиях на сервере — так же, как участник с теми же правами. Если разрешили владелец бота и сервер, бот ещё и модерирует: исключает, банит, выдаёт роли, управляет каналами и настройками сервера. Рядом с именем бота всегда стоит плашка «БОТ», а в его карточке видно владельца.
В личные сообщения бот пишет только тем, кто сам ему это разрешил, — подробно в разделе «Личные сообщения». Бот не бывает в группах и звонках, не заводит друзей и не вступает на серверы по приглашениям; в голосовых каналах серверов он может проигрывать звук — раздел «Голос». На сервер его добавляет человек с правом управлять этим сервером.
Это первая версия API. Несовместимые изменения мы объявляем на этой странице заранее.
Как создать бота
- В приложении Voxa откройте «Настройки» → «Боты» и нажмите «Создать бота».
- Имя пользователя бота — латиница, цифры и
_, от 3 до 24 символов, и в конце обязательноbot: например,weather_bot. Словоvoxaв имени оставлено для официальных ботов. - Отметьте, что умеет бот (разрешения — ниже), и примите Условия для разработчиков.
- Создавать ботов можно через сутки после регистрации и с подтверждённой почтой. У одного человека — до пяти ботов.
- Сразу после создания приложение покажет токен — один раз. Скопируйте его: больше мы его не покажем.
Переключатель «Могут добавлять все» делает бота публичным: добавить его на свой сервер сможет любой, у кого есть право управлять сервером. Иначе бота добавляете только вы. Бота добавляют в настройках сервера, во вкладке «Боты», — поиском по его @имени.
Токен
Каждый запрос к API подписывается токеном бота — в заголовке Authorization, со словом Bot перед ним:
Authorization: Bot vxb_…- Токен начинается с
vxb_— по этому началу его узнают сканеры утечек. СхемаBearerдля ботов не подходит: такой запрос получит 401. - «Новый токен» в карточке бота: старый перестаёт работать сразу, программа отключается от шлюза с кодом 4004.
- «Отозвать токен»: бот не сможет войти, пока вы не выпустите новый.
- Сервер хранит не сам токен, а его отпечаток, поэтому потерянный токен показать ещё раз нельзя — только выпустить новый.
- Храните токен как пароль: в переменной окружения или хранилище секретов, а не в коде, который кто-то увидит.
Адрес API
Все адреса ниже — относительно https://api.voxavoice.ru/api/bot/v1. Запросы и ответы — JSON в UTF-8, время — RFC 3339 в UTC, идентификаторы — UUID строкой.
curl -s https://api.voxavoice.ru/api/bot/v1/me \
-H "Authorization: Bot $VOXA_TOKEN"В примерах $VOXA_TOKEN — токен бота, а $SERVER, $CHANNEL и $MESSAGE — идентификаторы сервера, канала и сообщения: переменные оболочки, которые вы задаёте сами.
Разрешения и права на сервере
Что может бот, решают две вещи сразу: разрешения — что программа умеет вообще, их выбирает владелец бота, — и права роли бота на конкретном сервере, их выбирает тот, кто бота добавил. Нужно и то, и другое.
| Разрешение | Что даёт |
|---|---|
messages.read | Читать историю каналов и получать события сообщений и реакций |
messages.write | Писать, править и удалять свои сообщения, показывать «печатает» |
messages.manage | Удалять чужие сообщения — там, где у роли бота есть право управлять сообщениями |
reactions | Ставить и снимать реакции |
members.read | Видеть список участников и события о них |
members.manage | Исключать и банить участников, управлять ими в голосовых каналах — раздел «Модерация» |
roles.manage | Выдавать и снимать роли, создавать и менять роли ниже своей |
channels.manage | Создавать, менять и удалять каналы и их права — раздел «Управление сервером» |
server.manage | Менять название, иконку, баннер и описание сервера |
voice | Проигрывать звук в голосовых каналах серверов — раздел «Голос» |
Роль бота. Когда бота добавляют на сервер, у него появляется своя роль — в самом низу списка ролей, с правами, которые отметил добавивший. Выдать можно только те права, что есть у самого добавляющего. Роль бота правится во вкладке «Роли», как любая другая, и переопределения каналов для неё работают как обычно. К правам роли прибавляются права роли @everyone.
Администратором бот не бывает. Права администратора боту не выдаются, других ролей, кроме своей, у бота нет, а людям роль бота не выдать. Поэтому бота нельзя добавить на сервер, где у @everyone есть права администратора, и нельзя выдать их @everyone, пока на сервере есть боты.
Когда бота убирают с сервера, выгоняют или банят, его роль удаляется, а сообщения остаются. После добавления на сервер бот получает событие server_create.
Запросы
| Метод и адрес | Разрешение | Что делает |
|---|---|---|
GET /me | — | Сам бот, владелец, разрешения и адрес шлюза |
GET /servers | — | Серверы, на которых стоит бот |
GET /servers/{id} | — | Сервер: видимые боту каналы, категории, роли и права бота (my_permissions) |
GET /servers/{id}/members?after=&limit= | members.read | Участники по возрастанию id, от 1 до 200 за раз (по умолчанию 100); следующая страница — after=<id последнего> |
GET /servers/{id}/emojis | — | Эмодзи и стикеры сервера: id, name, url, animated, available |
GET /channels/{id}/messages?before=&limit= | messages.read | История: сообщения со seq меньше before, по возрастанию seq; limit от 1 до 100, по умолчанию 50. Вместо before — around=<seq>: окно вокруг сообщения, или after=<seq>: всё после него (раздел «Дочитывание») |
POST /channels/{id}/messages | messages.write | Отправить сообщение: content до 4000 символов, reply_to — id сообщения, на которое ответ, embeds — карточки, components — кнопки и меню, attachments — файлы |
PATCH /messages/{id} | messages.write | Исправить своё сообщение: content, embeds, components — любые из них |
DELETE /messages/{id} | своё — messages.write, чужое — messages.manage | Удалить сообщение |
GET /messages/{id}/reactions | messages.read | Реакции и первые 30 поставивших каждую |
POST /messages/{id}/reactions | reactions | Поставить реакцию: emoji (символ или :имя: эмодзи сервера) или emoji_id |
DELETE /messages/{id}/reactions?emoji= или ?emoji_id= | reactions | Снять свою реакцию |
POST /channels/{id}/typing | messages.write | Показать «печатает» в канале |
GET /gateway | — | Шлюз событий по WebSocket — следующий раздел |
GET /voice | voice | Голосовой сокет по WebSocket: войти в голосовой канал и проигрывать звук — раздел «Голос» |
POST /dms, GET /dms | messages.write, messages.read | Открыть личный разговор и узнать, кто разрешил писать, — раздел «Личные сообщения» |
GET, PUT /commands; GET, PUT /servers/{id}/commands; DELETE /commands/{id} | — | Команды через «/» — раздел «Команды» |
POST /interactions/{id}/callback, PATCH и DELETE …/original, POST …/followup | messages.write | Ответы на нажатия, команды и формы — раздел «Нажатия и ответы» |
POST /uploads | messages.write | Загрузить файл — раздел «Файлы» |
…/members/{user_id}, …/bans/…, …/roles/{role_id}, …/voice | members.manage, roles.manage | Исключения, баны, роли участника, голос — раздел «Модерация» |
PATCH /servers/{id}, …/channels, …/overwrites/…, …/roles | server.manage, channels.manage, roles.manage | Сервер, каналы, их права и роли — раздел «Управление сервером» |
В примерах ответы сокращены: у объектов бывают и другие поля (например, оформление профиля у user), а новые могут появиться. Не рассчитывайте на их отсутствие и не падайте на незнакомых.
Кто я. Бот, его владелец, разрешения и адрес шлюза:
curl -s https://api.voxavoice.ru/api/bot/v1/me \
-H "Authorization: Bot $VOXA_TOKEN"
{
"user": {
"id": "3f6c9e1a-…",
"username": "weather_bot",
"display_name": "Погода",
"avatar": null,
"bot": true
},
"owner_id": "91d2b7c4-…",
"scopes": ["messages.read", "messages.write", "members.read"],
"gateway": { "path": "/api/bot/v1/gateway", "heartbeat_secs": 30 }
}Серверы бота и один сервер целиком — только то, что боту видно:
curl -s https://api.voxavoice.ru/api/bot/v1/servers \
-H "Authorization: Bot $VOXA_TOKEN"
[
{ "id": "c47a0e55-…", "name": "Дом", "icon": null, "owner_id": "91d2b7c4-…" }
]curl -s https://api.voxavoice.ru/api/bot/v1/servers/$SERVER \
-H "Authorization: Bot $VOXA_TOKEN"
{
"id": "c47a0e55-…",
"name": "Дом",
"icon": null,
"owner_id": "91d2b7c4-…",
"banner": null,
"description": null,
"channels": [
{
"id": "e5b81f07-…",
"server_id": "c47a0e55-…",
"name": "общий",
"kind": "text",
"topic": null,
"position": 0,
"category_id": null,
"user_limit": null,
"is_news": false,
"overwrites": []
}
],
"categories": [],
"roles": [
{
"id": "7a3d…",
"server_id": "c47a0e55-…",
"name": "Погода",
"color": "#9aa3b2",
"permissions": 16512,
"position": 7,
"is_default": false,
"bot_user_id": "3f6c9e1a-…"
}
],
"my_permissions": 16512,
"last_seqs": { "e5b81f07-…": 1043 }
}Участники — нужно разрешение members.read:
curl -s "https://api.voxavoice.ru/api/bot/v1/servers/$SERVER/members?limit=100" \
-H "Authorization: Bot $VOXA_TOKEN"
[
{
"user": {
"id": "91d2b7c4-…",
"username": "kirill",
"display_name": "Кирилл",
"avatar": "/uploads/….png"
},
"nickname": null,
"joined_at": "2026-09-01T18:20:00Z",
"role_ids": []
}
]История канала. Страница идёт по возрастанию seq — номера сообщения. Он общий для всех каналов и только растёт, поэтому в одном канале номера идут с пропусками (раздел «Дочитывание»). Чтобы листать назад, передайте before=<seq первого>; before, around и after вместе — ошибка 400.
curl -s "https://api.voxavoice.ru/api/bot/v1/channels/$CHANNEL/messages?limit=50" \
-H "Authorization: Bot $VOXA_TOKEN"
[
{
"id": "0d9f6a2b-…",
"seq": 1043,
"channel_id": "e5b81f07-…",
"content": "!погода Москва",
"attachments": [],
"reply_to": null,
"reply_preview": null,
"created_at": "2026-09-24T12:00:00Z",
"edited_at": null,
"author": {
"id": "91d2b7c4-…",
"username": "kirill",
"display_name": "Кирилл",
"avatar": null
}
}
]Отправить сообщение. Файлы прикрепляют полем attachments — раздел «Файлы». Поле, которого нет в описании, — ошибка 400, и при отправке, и при правке: опечатка вроде embed вместо embeds иначе молча ушла бы сообщением без карточки.
curl -s -X POST https://api.voxavoice.ru/api/bot/v1/channels/$CHANNEL/messages \
-H "Authorization: Bot $VOXA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"content": "В Москве +12, облачно", "reply_to": "0d9f6a2b-…"}'
{
"id": "5c2e90d1-…",
"seq": 1044,
"channel_id": "e5b81f07-…",
"content": "В Москве +12, облачно",
"attachments": [],
"reply_to": "0d9f6a2b-…",
"reply_preview": { "author_name": "Кирилл", "content": "!погода Москва", "has_attachments": false },
"created_at": "2026-09-24T12:00:01Z",
"edited_at": null,
"author": {
"id": "3f6c9e1a-…",
"username": "weather_bot",
"display_name": "Погода",
"avatar": null,
"bot": true
}
}Исправить и удалить. Исправить можно только своё сообщение; ответ — сообщение целиком, с edited_at.
curl -s -X PATCH https://api.voxavoice.ru/api/bot/v1/messages/$MESSAGE \
-H "Authorization: Bot $VOXA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"content": "В Москве +13, облачно"}'curl -s -X DELETE https://api.voxavoice.ru/api/bot/v1/messages/$MESSAGE \
-H "Authorization: Bot $VOXA_TOKEN"
{ "ok": true }Реакции. Эмодзи в адресе снятия реакции кодируется как часть адреса:
curl -s -X POST https://api.voxavoice.ru/api/bot/v1/messages/$MESSAGE/reactions \
-H "Authorization: Bot $VOXA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"emoji": "👍"}'
{ "emoji": "👍", "count": 3, "mine": true }curl -s -G -X DELETE https://api.voxavoice.ru/api/bot/v1/messages/$MESSAGE/reactions \
-H "Authorization: Bot $VOXA_TOKEN" \
--data-urlencode "emoji=👍"
{ "emoji": "👍", "count": 2, "mine": false }curl -s https://api.voxavoice.ru/api/bot/v1/messages/$MESSAGE/reactions \
-H "Authorization: Bot $VOXA_TOKEN"
[
{
"emoji": "👍",
"count": 2,
"mine": false,
"users": [
{ "id": "91d2b7c4-…", "username": "kirill", "display_name": "Кирилл", "avatar": null }
]
}
]Свои эмодзи сервера. В тексте сообщения пишите :имя: — сервер сам свяжет его с эмодзи сервера канала, и люди увидят картинку (связь лежит в meta.emojis сообщения). Эмодзи других серверов бот не использует. Реакция своим эмодзи — {"emoji": ":party_cat:"} или {"emoji_id": "…"}; в ответе и в событии message_reaction появляются emoji_id, url и animated. Отличайте реакции по emoji_id, если он есть: эмодзи могут переименовать.
Список — GET /servers/{id}/emojis. Эмодзи с available: false сейчас не помещается в места по уровню сервера: его :имя: в тексте остаётся буквами, а новую реакцию им не поставить — поддержать уже стоящую можно.
curl -s -X POST https://api.voxavoice.ru/api/bot/v1/messages/$MESSAGE/reactions \
-H "Authorization: Bot $VOXA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"emoji": ":party_cat:"}'
{ "emoji": ":party_cat:", "count": 1, "mine": true, "emoji_id": "0b7c…", "url": "/uploads/5e1a….webp", "animated": false }«Печатает». Показывается недолго; если ответ готовится дольше, повторяйте.
curl -s -X POST https://api.voxavoice.ru/api/bot/v1/channels/$CHANNEL/typing \
-H "Authorization: Bot $VOXA_TOKEN"
{ "ok": true }Шлюз событий
События приходят по WebSocket: wss://api.voxavoice.ru/api/bot/v1/gateway. Токен — тем же заголовком Authorization: Bot vxb_…. Если библиотека не умеет заголовки (браузерный WebSocket), передайте токен параметром ?token=vxb_…, но заголовок надёжнее: адрес с параметром может осесть в чужих журналах по дороге.
Отказ приходит обычным HTTP-ответом, без перехода на WebSocket, с тем же телом, что у запросов: 401 — токен неверный или отозван, 403 — бот отключён администрацией или заблокирован.
websocat -H "Authorization: Bot $VOXA_TOKEN" \
wss://api.voxavoice.ru/api/bot/v1/gatewayКаждый кадр — JSON {"t": тип, "d": данные}. Первым приходит ready:
{"t":"ready","d":{
"user": { "id": "3f6c9e1a-…", "username": "weather_bot", "display_name": "Погода", "avatar": null, "bot": true },
"servers": [ { "id": "c47a0e55-…", "name": "Дом", "icon": null, "owner_id": "91d2b7c4-…" } ],
"scopes": ["messages.read", "messages.write", "members.read"],
"heartbeat_secs": 30,
"gateway_version": 1,
"resume": { "last_seq": 1043, "at": "2026-09-24T12:00:00Z" }
}}| t | Когда приходит | Нужно |
|---|---|---|
message_create | Новое сообщение в видимом боту канале; в личном разговоре — только с тем, кто разрешил боту писать (server_id: null) | messages.read |
message_update | Сообщение исправили | messages.read |
message_delete | Сообщение удалили: {id, channel_id, server_id} | messages.read |
message_purge | Канал очистили: {channel_id, server_id, count, ids}; ids: null — перечитайте канал | messages.read |
message_reaction | Реакцию поставили или сняли (у своего эмодзи — emoji_id, url) | messages.read |
message_pin | Сообщение закрепили или открепили | messages.read |
member_join | На сервер пришёл участник | members.read |
member_leave | Участник ушёл; про самого бота — всегда | members.read |
member_update | У участника сменились роли; про самого бота — всегда | members.read |
user_update | Участник сменил имя или аватар | members.read |
channel_create, channel_update, channel_delete | Каналы сервера | — |
category_create, category_update, category_delete | Категории | — |
role_create, role_update, role_delete | Роли | — |
server_create | Бота добавили на сервер: {id} | — |
server_update, server_delete | Сервер изменили или удалили | — |
interaction_create | Нажали кнопку, выбрали в меню, вызвали команду или отправили форму — раздел «Нажатия и ответы» | — |
dm_consent | Человек разрешил или запретил боту писать ему в личные: {user, channel_id, allowed} | — |
pong | Ответ на ваш ping | — |
disconnect | Сервер закрывает соединение: {code, reason} | — |
Формы данных — те же, что в ответах на запросы: в message_create и message_update лежит message целиком, в member_join — member. Личные сообщения приходят только из разговоров с теми, кто разрешил боту писать. «Кто в сети», «печатает», голос, звонки, группы, друзья и уведомления боту не приходят никогда.
{"t":"message_create","d":{"server_id":"c47a0e55-…","channel_kind":"text","message":{"id":"0d9f6a2b-…","seq":1043,"channel_id":"e5b81f07-…","content":"!погода Москва","attachments":[],"reply_to":null,"reply_preview":null,"created_at":"2026-09-24T12:00:00Z","edited_at":null,"author":{"id":"91d2b7c4-…","username":"kirill","display_name":"Кирилл","avatar":null}}}}
{"t":"message_reaction","d":{"server_id":"c47a0e55-…","channel_id":"e5b81f07-…","message_id":"5c2e90d1-…","emoji":"👍","user_id":"91d2b7c4-…","added":true,"count":3}}
{"t":"message_reaction","d":{"server_id":"c47a0e55-…","channel_id":"e5b81f07-…","message_id":"5c2e90d1-…","emoji":":party_cat:","user_id":"91d2b7c4-…","added":true,"count":1,"emoji_id":"0b7c…","url":"/uploads/5e1a….webp","animated":false}}
{"t":"member_join","d":{"server_id":"c47a0e55-…","member":{"user":{"id":"a18e…","username":"lena","display_name":"Лена","avatar":null},"nickname":null,"joined_at":"2026-09-24T12:05:00Z","role_ids":[]}}}
{"t":"server_create","d":{"id":"5e07…"}}
{"t":"disconnect","d":{"code":4004,"reason":"token_rotated"}}Живость. Сервер шлёт WebSocket Ping раз в 30 секунд, библиотеки отвечают на него сами. Любой кадр от вас продлевает соединение; 75 секунд тишины — сервер закроет его с кодом 4008. Проверить связь можно и кадром ping. Другие входящие кадры сервер пропускает, кадр больше 4 КБ закрывает соединение с кодом 1009.
→ {"t":"ping"}
← {"t":"pong","d":{}}| Код | Почему | Что делать |
|---|---|---|
| 4003 | Бот отключён администрацией | Не переподключаться; владелец получил уведомление с причиной |
| 4004 | Токен заменён или отозван | Не переподключаться с этим токеном |
| 4005 | Бот удалён | Остановиться |
| 4006 | Бот или его владелец заблокирован | Остановиться |
| 4007 | Владелец изменил разрешения бота | Переподключиться сразу и перечитать GET /me |
| 4008 | 75 секунд тишины | Переподключиться |
| 4009 или обрыв без кадра | Бот не успевал читать события, шестое подключение вытеснило старое или сервер перезапустился | Переподключиться с паузой и дочитать историю |
После переподключения пропущенные события не приходят заново — дочитайте их историей с after=<последний seq>, как описано в разделе «Дочитывание». Переподключайтесь с растущей паузой — от секунды до минуты, со случайным разбросом, чтобы не бить в сервер все разом.
Бот на Node.js — отвечает pong на !ping:
// npm i ws — Node 18+; fetch встроен
import WebSocket from "ws";
const TOKEN = process.env.VOXA_TOKEN;
const API = "https://api.voxavoice.ru/api/bot/v1";
const auth = { Authorization: `Bot ${TOKEN}` };
const ws = new WebSocket("wss://api.voxavoice.ru/api/bot/v1/gateway", { headers: auth });
ws.on("message", async (raw) => {
const { t, d } = JSON.parse(raw.toString());
if (t !== "message_create") return;
const m = d.message;
if (m.author.bot) return; // ботам не отвечаем — и себе тоже
if (m.content.trim() !== "!ping") return;
await fetch(`${API}/channels/${m.channel_id}/messages`, {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({ content: "pong", reply_to: m.id }),
});
});
ws.on("close", (code) => console.log("шлюз закрыт", code));Личные сообщения
Бот может писать человеку в личные — но только тому, кто сам это разрешил. Разрешение даёт человек, и только явным действием: кнопкой «Разрешить писать мне» или «Написать» в профиле бота, своим сообщением боту, командой или нажатием кнопки в переписке с ботом. Запретить можно в любой момент — в профиле бота или в меню чата; блокировка бота тоже запрещает. Отдельного разрешения (скоупа) для личных нет: они доступны боту с messages.read и messages.write.
Человек может написать боту всегда — дружба не нужна. Бот без разрешения не откроет разговор и не ответит в него: запрос получит 403. Группы ботам закрыты по-прежнему.
| Метод и адрес | Разрешение | Что делает |
|---|---|---|
POST /dms | messages.read и messages.write | Открыть разговор с человеком, который разрешил: {"user_id"}. Ответ — channel_id, user и created (разговор новый или уже был) |
GET /dms?after=&limit= | messages.read | Кто разрешил писать: по возрастанию id, от 1 до 200 за раз (по умолчанию 100), с channel_id (null — разговора ещё нет), granted_at и last_seq — seq последнего сообщения разговора (null — сообщений нет) |
curl -s -X POST https://api.voxavoice.ru/api/bot/v1/dms \
-H "Authorization: Bot $VOXA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"user_id": "91d2b7c4-…"}'
{
"channel_id": "b3a1c2d4-…",
"user": { "id": "91d2b7c4-…", "username": "kirill", "display_name": "Кирилл", "avatar": null },
"created": true
}curl -s "https://api.voxavoice.ru/api/bot/v1/dms?limit=100" \
-H "Authorization: Bot $VOXA_TOKEN"
[
{
"user": { "id": "91d2b7c4-…", "username": "kirill", "display_name": "Кирилл", "avatar": null },
"channel_id": "b3a1c2d4-…",
"granted_at": "2026-09-24T18:00:00Z"
}
]Дальше — обычные запросы: POST /channels/{channel_id}/messages, PATCH /messages/{id}, POST /channels/{id}/typing. В шлюз приходят события только ваших разговоров и только с теми, кто разрешил: message_create, message_update, message_delete, message_reaction с server_id: null. Когда человек разрешает или запрещает, приходит dm_consent — адресно вам. Почему запретили, бот не узнаёт; после запрета разговор остаётся у человека, а бот перестаёт его видеть.
{"t":"dm_consent","d":{"user":{"id":"91d2b7c4-…","username":"kirill","display_name":"Кирилл","avatar":null},"channel_id":"b3a1c2d4-…","allowed":true}}
{"t":"message_create","d":{"server_id":null,"message":{"id":"7e0c5a11-…","seq":12,"channel_id":"b3a1c2d4-…","content":"какая завтра погода?","attachments":[],"reply_to":null,"reply_preview":null,"created_at":"2026-09-24T18:00:05Z","edited_at":null,"author":{"id":"91d2b7c4-…","username":"kirill","display_name":"Кирилл","avatar":null}}}}
{"t":"dm_consent","d":{"user":{"id":"91d2b7c4-…","username":"kirill","display_name":"Кирилл","avatar":null},"channel_id":"b3a1c2d4-…","allowed":false}}Разговор, начатый ботом. Если человек ничего не писал и не нажимал в разговоре больше 24 часов, сообщение бота считается началом нового разговора и попадает под строгий предел: не больше 60 в час на бота и не больше 10 в сутки одному человеку. Новых разговоров, которые бот открывает сам (POST /dms), — не больше 30 в час. Ответы на то, что человек только что написал или нажал, под этот предел не попадают. Сверх предела — 429 «слишком много новых разговоров в личных — подождите …».
Разрешение — не повод для рассылки. Пишите в личные то, о чём человек просил: ответ, напоминание, результат. Реклама и рассылки, о которых не просили, запрещены Условиями для разработчиков — за них бота отключают.
Команды через /
Человек набирает / в поле ввода — приложение показывает команды ботов этого канала с описаниями и подсказками опций, а для участника, канала и роли — выбор из списка. Вызов приходит боту событием interaction_create с type: "command" (раздел «Нажатия и ответы»). Над ответом бота в ленте видно, кто какую команду вызвал: «Кирилл · /погода».
Регистрация. Программа отправляет свой список команд целиком — обычно при каждом запуске. Команды с теми же именами сохраняют id, пропавшие из списка удаляются, всё — одной транзакцией. Тот же самый список сервер не перезаписывает и людям не рассылает, поэтому регистрироваться на каждом старте можно без опаски. Разрешения (скоупа) для команд не нужно.
| Метод и адрес | Что делает |
|---|---|
GET /commands | Общие команды бота — для всех его серверов и личных |
PUT /commands | Заменить все общие команды списком (до 100) |
GET /servers/{id}/commands | Команды только для этого сервера |
PUT /servers/{id}/commands | Заменить команды этого сервера списком (до 100); бот должен быть на сервере |
DELETE /commands/{id} | Удалить одну свою команду — общую или серверную |
| Поле | Что это |
|---|---|
name | Имя: строчные латиница и кириллица, цифры, _ и -, до 32 символов — погода, weather_now |
description | Описание в меню, 1–100 символов |
dm | Показывать ли команду в личных с ботом. Только у общих команд. По умолчанию true, а у команды с required_permissions — false: писать dm: false рядом с правами не нужно |
required_permissions | Биты прав (например, 2 — управлять сервером): без них человек команду не увидит и не вызовет. null — всем, кто может писать в канал. В личных прав нет, поэтому такая команда там не показывается, а явное dm: true вместе с правами — 400 |
options | Опции, до 25; обязательные — раньше необязательных |
| Поле | Что это |
|---|---|
type | string, integer, number, boolean, user, channel или role |
name, description | Как у команды; имя уникально в команде |
required | Обязательная ли; по умолчанию false |
choices | До 25 вариантов {name, value} — только у string, integer и number; человек выбирает из списка |
min_value, max_value | Пределы числа, по модулю не больше 2^53 |
min_length, max_length | Пределы длины текста: 0–1000, по умолчанию 0 и 1000 |
[
{
"name": "погода",
"description": "Погода в городе",
"options": [
{ "type": "string", "name": "город", "description": "Какой город",
"required": true, "min_length": 2, "max_length": 100 },
{ "type": "string", "name": "когда", "description": "Сегодня или завтра",
"choices": [ { "name": "Сегодня", "value": "today" }, { "name": "Завтра", "value": "tomorrow" } ] }
]
},
{
"name": "настроить",
"description": "Куда присылать прогноз на этом сервере",
"dm": false,
"required_permissions": 2,
"options": [
{ "type": "channel", "name": "канал", "description": "Канал для прогноза", "required": true }
]
}
]curl -s -X PUT https://api.voxavoice.ru/api/bot/v1/commands \
-H "Authorization: Bot $VOXA_TOKEN" \
-H "Content-Type: application/json" \
-d @commands.json
[
{
"id": "0a9e41c3-…",
"bot_id": "3f6c9e1a-…",
"server_id": null,
"name": "погода",
"description": "Погода в городе",
"dm": true,
"required_permissions": null,
"options": [ … ],
"created_at": "2026-09-24T18:00:00Z",
"updated_at": "2026-09-24T18:00:00Z"
},
…
]- Серверная команда перекрывает общую команду с тем же именем на своём сервере.
- Команду бота видят только те, кто может писать в канал, и только если сам бот видит канал и может в него писать.
- Ошибка в списке — 400 с указанием места: «команда 3 «опрос», опция «варианты»: описание 1–100 символов».
- Меню у людей обновляется само: после изменения списка им приходит событие. Менять список можно не чаще 20 раз в час.
Кнопки и меню
Под сообщением бота можно поставить кнопки и меню выбора — полем components при отправке или правке. До 5 рядов; в ряду — до 5 кнопок или одно меню; всего в сообщении — до 25 элементов.
| style | Как выглядит и когда брать |
|---|---|
primary | Акцентная — главное действие |
secondary | Обычная; стиль по умолчанию |
success | Зелёная — «да», «готово», «иду» |
danger | Красная — удалить, отменить, завершить |
link | Ссылка: открывает url после подтверждения человека, нажатия боту не приходят |
| Поле | Что это |
|---|---|
label | Подпись кнопки, 1–80 символов; у кнопки нужна подпись или эмодзи |
emoji | Одна эмодзи, до 16 символов, без букв и цифр (цифра — только клавишей-эмодзи: 1️⃣) |
custom_id | 1–100 символов, уникален в сообщении; его бот получит при нажатии. У кнопки-ссылки его нет |
url | Только у link: http или https, до 512 символов |
disabled | Отключена: видна, но не нажимается |
placeholder | Подпись меню, пока ничего не выбрано, до 150 символов |
min_values, max_values | Сколько вариантов меню выбрать: от 0 до 25 и от 1 до 25, по умолчанию 1 и 1 |
options | Варианты меню, 1–25: label 1–100, value 1–100 (уникально в меню), description до 100, emoji, default — отмечен заранее |
curl -s -X POST https://api.voxavoice.ru/api/bot/v1/channels/$CHANNEL/messages \
-H "Authorization: Bot $VOXA_TOKEN" \
-H "Content-Type: application/json" \
-d @- <<'EOF'
{
"content": "Идём в кино в пятницу?",
"components": [
{ "type": "row", "components": [
{ "type": "button", "style": "success", "label": "Иду", "emoji": "🎬", "custom_id": "movie:yes" },
{ "type": "button", "style": "secondary", "label": "Не смогу", "custom_id": "movie:no" },
{ "type": "button", "style": "link", "label": "Афиша", "url": "https://example.com/afisha" }
]},
{ "type": "row", "components": [
{ "type": "select", "custom_id": "movie:time", "placeholder": "Во сколько удобно?",
"min_values": 1, "max_values": 2,
"options": [
{ "label": "18:00", "value": "18" },
{ "label": "20:00", "value": "20" },
{ "label": "22:00", "value": "22", "description": "Поздний сеанс" }
] }
]}
]
}
EOFПравка. PATCH /messages/{id} принимает content, embeds и components — каждое по желанию: поля нет — остаётся как было, [] — убрать. Если поменялись только кнопки (отключили, перекрасили), отметка «изменено» не ставится: слова те же.
curl -s -X PATCH https://api.voxavoice.ru/api/bot/v1/messages/$MESSAGE \
-H "Authorization: Bot $VOXA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"components": []}'custom_id видят все, кто видит сообщение. Не кладите туда секретов и не считайте его пропуском: права того, кто нажал, проверяйте по member в событии. Если бот наказывает или выдаёт роли по просьбе человека, сверяйте и его ранг (раздел «Модерация»).
Сообщение из одних кнопок, без текста и карточек, сервер не примет (400): старые версии приложения кнопок не показывают и нарисовали бы пустое сообщение.
Карточки
Карточка — оформленный блок в сообщении: заголовок-ссылка, текст с разметкой, поля, картинки, подвал и цветная полоса слева. До 10 карточек в сообщении, поле embeds.
| Поле | Предел |
|---|---|
title | До 256 символов |
description | До 4096; разметка как в сообщениях |
url | Ссылка заголовка: http или https |
color | Число 0xRRGGBB от 0 до 16777215; нет — нейтральная полоса |
fields | До 25: name 1–256, value 1–1024, inline — в строку, а не столбиком |
thumbnail.url, image.url | Миниатюра справа и картинка под текстом: https |
footer | text 1–2048 и icon_url |
author | name 1–256, url и icon_url |
timestamp | Время в RFC 3339 — покажется в подвале |
- Весь текст карточек одного сообщения — не больше 6000 символов. Карточка без заголовка, текста, полей, картинок и автора — 400.
- Ссылки — только http и https, без логина и пароля в адресе.
- Картинки — только https или файл этого же сервера
/uploads/…: свой файл бота, открытая картинка Voxa (аватар, значок, эмодзи) или вложение из канала, который бот видит; иначе 400. Внешнюю картинку человек видит после «Показать» или если сам включил «всегда показывать»: иначе каждая карточка сообщала бы владельцу бота адрес любого, кто пролистал ленту. - Разметка — та же, что в сообщениях: жирный,
код, ссылки вида[текст](адрес). HTML не работает.
{
"embeds": [
{
"author": { "name": "Погода", "icon_url": "https://example.com/sun.png" },
"title": "Москва, завтра",
"url": "https://example.com/moscow",
"description": "**+12°**, дождь после обеда",
"color": 5793266,
"fields": [
{ "name": "Ветер", "value": "5 м/с", "inline": true },
{ "name": "Влажность", "value": "80%", "inline": true }
],
"thumbnail": { "url": "https://example.com/rain.png" },
"footer": { "text": "Данные обновляются раз в час" },
"timestamp": "2026-09-24T18:00:00Z"
}
]
}Текст для старых версий. Если текста в сообщении нет, а карточки есть, сервер сам соберёт content из карточек: автор, заголовок, описание, поля «название: значение», подвал; у карточки из одной картинки — ссылка на картинку (или слово «Картинка», если она из ваших вложений). Этот же текст увидят уведомления, поиск и старые версии приложения; новые его не рисуют — в meta.fallback стоит true. Для примера выше:
Погода
Москва, завтра
**+12°**, дождь после обеда
Ветер: 5 м/с
Влажность: 80%
Данные обновляются раз в часНажатия и ответы
Нажатие кнопки, выбор в меню, вызов команды и отправка формы приходят боту событием interaction_create — адресно, без скоупов. В нём: кто (user, а на сервере ещё member с ролями и правами в этом канале), где (server_id, channel_id), что (data), сообщение с кнопкой (message), права самого бота в канале (app_permissions) и одноразовый токен ответа token.
{"t":"interaction_create","d":{
"id": "5b1f7c2e-…",
"token": "vxi_…",
"type": "command",
"server_id": "c47a0e55-…",
"channel_id": "e5b81f07-…",
"user": { "id": "91d2b7c4-…", "username": "kirill", "display_name": "Кирилл", "avatar": null },
"member": { "nickname": null, "role_ids": [], "permissions": 16512 },
"app_permissions": 16512,
"data": {
"command_id": "0a9e41c3-…",
"name": "погода",
"options": [ { "name": "город", "type": "string", "value": "Москва" } ],
"resolved": { "users": {}, "members": {}, "channels": {}, "roles": {} }
},
"message": null,
"expires_at": "2026-09-24T18:15:00Z",
"respond_by": "2026-09-24T18:00:03Z"
}}// нажатие кнопки или выбор в меню — "type": "component", в "message" — сообщение с кнопкой
"data": { "custom_id": "movie:time", "component_type": "select", "values": ["18", "20"] }
// отправка формы — "type": "modal_submit"
"data": { "custom_id": "feedback", "fields": [ { "custom_id": "text", "value": "Всё понравилось" } ] }Три секунды. На первый ответ у бота 3 секунды (respond_by). Не успели — человек увидит «Бот не ответил», а опоздавший ответ получит 409. Если работа долгая, сразу скажите «думаю» (defer) и заполните ответ позже: токен живёт 15 минут (expires_at). Если бот не в сети, человек сразу получит «Бот сейчас не в сети — попробуйте позже», а событие не придёт.
| type | Когда можно | Что происходит |
|---|---|---|
reply | Всегда | Сообщение от бота в канал (у ответа на команду — со строкой «кто вызвал») или, с "ephemeral": true, «видно только вам». data: content, embeds, components, ephemeral |
defer | Всегда | У человека строка «{бот} думает…»; сам ответ — PATCH …/original. data: ephemeral |
update | Нажатие; форма, открытая нажатием | Правка сообщения с кнопкой, без отметки «изменено». data: content, embeds, components |
defer_update | То же, что update | Кнопка перестаёт ждать; правка — позже через PATCH …/original |
modal | Команда, нажатие | У человека открывается форма — раздел «Формы» |
| Метод и адрес | Тело | Что делает |
|---|---|---|
POST /interactions/{id}/callback | {"token", "type", "data"} | Первый ответ — ровно один; второй получит 409 |
PATCH /interactions/{id}/original | {"token", "content", "embeds", "components"} | После defer — создать ответ, после reply или update — поправить его |
POST /interactions/{id}/followup | {"token", "content", "embeds", "components", "ephemeral"} | Ещё один ответ — до 5 на одно нажатие |
DELETE /interactions/{id}/original | {"token"} | Убрать исходный ответ, когда он отжил своё («Отменено», «Готово»): сообщение в канале удаляется, «видно только вам» пропадает у человека. У update исходный ответ — само сообщение с кнопкой. Дополнительные ответы после этого можно, править исходный — уже нет |
- Нужно разрешение
messages.write. Без него нажатия и команды до бота не доходят: человек сразу видит, что бот не может отвечать. Токен — в теле запроса, а не в адресе: адреса оседают в журналах. - Ответ в канал проходит те же проверки, что обычная отправка: права бота в канале, стоп-слова, предел сообщений в канал.
- Ответы на нажатия считаются отдельно от записей: 600 в минуту на бота, чтобы опрос на полсотни человек не упирался в предел.
curl -s -X POST https://api.voxavoice.ru/api/bot/v1/interactions/$INTERACTION/callback \
-H "Authorization: Bot $VOXA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"token": "vxi_…", "type": "reply", "data": {"embeds": [{"title": "Москва", "description": "**+12°**, облачно", "color": 5793266}]}}'
{ "ok": true, "message": { "id": "8d2f…", "content": "Москва\n**+12°**, облачно", "meta": { "kind": "bot", "v": 1, … }, … } }Цикл «нажали → обновили». Человек нажал кнопку под сообщением — бот правит это же сообщение и отдельно отвечает только ему:
# человек нажал «Иду» под сообщением $MESSAGE — отмечаем и выключаем кнопку
curl -s -X POST https://api.voxavoice.ru/api/bot/v1/interactions/$INTERACTION/callback \
-H "Authorization: Bot $VOXA_TOKEN" \
-H "Content-Type: application/json" \
-d @- <<'EOF'
{
"token": "vxi_…",
"type": "update",
"data": {
"content": "Идём в кино в пятницу? Уже идут: 3",
"components": [
{ "type": "row", "components": [
{ "type": "button", "style": "success", "label": "Иду", "emoji": "🎬", "custom_id": "movie:yes" },
{ "type": "button", "style": "secondary", "label": "Не смогу", "custom_id": "movie:no" }
]}
]
}
}
EOF
# и отдельно — только нажавшему
curl -s -X POST https://api.voxavoice.ru/api/bot/v1/interactions/$INTERACTION/followup \
-H "Authorization: Bot $VOXA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"token": "vxi_…", "content": "Вы в списке!", "ephemeral": true}'# сначала «думаю» — у человека строка «Погода думает…»
curl -s -X POST https://api.voxavoice.ru/api/bot/v1/interactions/$INTERACTION/callback \
-H "Authorization: Bot $VOXA_TOKEN" -H "Content-Type: application/json" \
-d '{"token": "vxi_…", "type": "defer"}'
# потом, в пределах 15 минут, — сам ответ
curl -s -X PATCH https://api.voxavoice.ru/api/bot/v1/interactions/$INTERACTION/original \
-H "Authorization: Bot $VOXA_TOKEN" -H "Content-Type: application/json" \
-d '{"token": "vxi_…", "content": "В Москве завтра +12, дождь после обеда"}'curl -s -X DELETE https://api.voxavoice.ru/api/bot/v1/interactions/$INTERACTION/original \
-H "Authorization: Bot $VOXA_TOKEN" -H "Content-Type: application/json" \
-d '{"token": "vxi_…"}'
{ "ok": true }// тот же bot.mjs: ответы на команды и кнопки
ws.on("message", async (raw) => {
const { t, d } = JSON.parse(raw.toString());
if (t !== "interaction_create") return;
// На первый ответ — 3 секунды. Долгое — сначала {"type": "defer"}.
const answer =
d.type === "command" && d.data.name === "погода"
? { type: "reply", data: { content: "В Москве +12, облачно" } }
: { type: "reply", data: { content: "Эта кнопка больше не работает", ephemeral: true } };
await fetch(`${API}/interactions/${d.id}/callback`, {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({ token: d.token, ...answer }),
});
});Формы
Форма открывается у человека ответом на нажатие или команду (type: "modal"). Её отправка приходит боту новым interaction_create с type: "modal_submit"; на отправку формы формой ответить нельзя.
| Поле | Предел |
|---|---|
custom_id | 1–100 символов — придёт с отправкой |
title | Заголовок, 1–45 символов |
fields | От 1 до 5 полей ввода |
fields[].custom_id, label | 1–100 (уникален в форме) и подпись 1–45 |
fields[].style | short — строка, paragraph — абзац; по умолчанию short |
fields[].required | Обязательное ли; по умолчанию true |
fields[].placeholder | Подсказка в пустом поле, до 100 символов |
fields[].min_length, max_length | 0–4000 и 1–4000, по умолчанию 0 и 4000 |
fields[].value | Предзаполнение, не длиннее max_length |
curl -s -X POST https://api.voxavoice.ru/api/bot/v1/interactions/$INTERACTION/callback \
-H "Authorization: Bot $VOXA_TOKEN" \
-H "Content-Type: application/json" \
-d @- <<'EOF'
{
"token": "vxi_…",
"type": "modal",
"data": {
"custom_id": "feedback",
"title": "Отзыв о сеансе",
"fields": [
{ "custom_id": "text", "label": "Что понравилось?", "style": "paragraph",
"placeholder": "Пара слов", "max_length": 1000 },
{ "custom_id": "score", "label": "Оценка от 1 до 5", "style": "short",
"required": false, "max_length": 1 }
]
}
}
EOFСервер сверяет отправку с выданной формой: поля ровно те, обязательные заполнены, длины в пределах. Значения приходят без пробелов по краям, пустое необязательное — пустой строкой. Форма одноразовая, выдана одному человеку и живёт 15 минут.
Видно только вам
Ответ с "ephemeral": true (в reply, defer или followup) видит только тот, кто нажал или вызвал команду. В базу он не пишется, в историю, поиск, уведомления и непрочитанное не попадает и пропадает после перезапуска приложения. Под таким ответом тоже могут быть кнопки: их нажатие придёт с message.ephemeral: true, update поправит сам ответ, а DELETE …/original уберёт его у человека.
Берите «видно только вам» для подтверждений, ошибок, личных списков и настроек — канал не засоряется. Кнопки и команды есть только в новых версиях приложения, поэтому такой ответ до нажавшего доходит всегда.
Модерация
Бот может исключать и банить участников, выдавать и снимать роли, заглушать, отключать и переносить людей в голосовых каналах — если это разрешают и его разрешения, и права его роли на сервере. Правила те же, что у человека с такой ролью, и проверяет их тот же код: ни своих послаблений, ни своих запретов у бота нет.
| Метод и адрес | Разрешение и право роли | Что делает |
|---|---|---|
DELETE /servers/{id}/members/{user_id}?reason= | members.manage, «Исключение участников» | Исключить участника |
GET /servers/{id}/bans | members.manage, «Бан участников» | Список банов сервера |
PUT /servers/{id}/bans/{user_id} | members.manage, «Бан участников» | Забанить; тело {"reason"} можно не присылать. Участника бан ещё и исключает; забанить можно и того, кого на сервере нет |
DELETE /servers/{id}/bans/{user_id}?reason= | members.manage, «Бан участников» | Снять бан |
PUT /servers/{id}/members/{user_id}/roles/{role_id}?reason= | roles.manage, «Управление ролями» | Выдать одну роль. В ответе role_ids — все роли участника после изменения |
DELETE /servers/{id}/members/{user_id}/roles/{role_id}?reason= | roles.manage, «Управление ролями» | Снять одну роль |
PATCH /servers/{id}/members/{user_id}/voice | members.manage, «Мьют в голосе» | Голос участника: mute, deafen, channel_id (id канала — перенести, null — отключить), reason |
Всё решает место роли бота. Бот исключает, банит, выдаёт и снимает роли только тем, чья высшая роль ниже высшей роли бота, а выдаёт только роли ниже своей и без прав, которых у него нет. Роль бота появляется в самом низу списка: пока администратор не поднимет её в «Настройки сервера» → «Роли», бот не справится ни с кем, у кого есть хоть одна роль. Отказ приходит 403 тем же текстом, что человеку: «нельзя исключить участника, чья роль выше или равна вашей», «можно выдавать и снимать только роли ниже вашей высшей». Владельца сервера не исключит и не забанит никто.
- Причина (
reason, до 500 символов) попадает в журнал аудита сервера. Все действия бота записываются туда от его имени, с плашкой «БОТ»: администраторы видят, кто это сделал и почему. - Бот действует своей ролью, а не ролью того, кто попросил. Если исключение, бан или роль идут по команде или кнопке, сервер сверит иерархию с ролью бота — а она обычно выше, чем у модераторов. Проверьте сами и право, и ранг того, кто вызвал: его высшая роль (
member.role_idsв событии, позиции — изGET /servers/{id}, меньшая позиция — выше) должна быть строго выше высшей роли цели (data.resolved.members), а владельца сервера не трогайте. Иначе младший модератор накажет старшего руками бота. Пример —mayPunishв шаблоне библиотеки. - Роли — по одной. Заменить все роли участника разом бот не может: так кнопка «взять роль» не спорит с человеком, который в ту же секунду меняет роли. Выдать уже выданную или снять невыданную — не ошибка: ответ тот же, а новой записи в журнале нет.
- Голос — по шагам: сначала
mute, потомdeafen, потомchannel_id. Если шаг не прошёл, следующие не выполняются, а сделанные остаются. Нужно хотя бы одно поле. Участник должен сидеть в голосовом канале этого сервера, иначе 409. Право «Мьют в голосе» нужно в канале, где сидит участник; чтобы перенести, бот должен ещё видеть канал назначения и мочь в него войти. - В голосе равная роль — не помеха. Заглушить, выключить звук, вернуть их, перенести и отключить от голоса бот может и того, чья высшая роль стоит вровень с ролью бота. Того, чья роль выше, — нет (403 «нельзя заглушить участника, чья роль выше вашей»), владельца сервера — никогда. Исключить и забанить по-прежнему можно только того, кто строго ниже. С ролями проверяется сама роль, а не человек: выдать или снять можно только роль ниже высшей роли бота, и ранг того, кому её выдают, тут не важен. Модераторы-люди живут по тем же правилам.
- Бан не удаляет старые сообщения — у людей такой возможности тоже нет.
$MEMBERи$ROLEв примерах — id участника и роли (раздел «Как узнать ID»).
curl -s -G -X DELETE https://api.voxavoice.ru/api/bot/v1/servers/$SERVER/members/$MEMBER \
-H "Authorization: Bot $VOXA_TOKEN" \
--data-urlencode "reason=реклама в личных"
{ "ok": true }curl -s -X PUT https://api.voxavoice.ru/api/bot/v1/servers/$SERVER/bans/$MEMBER \
-H "Authorization: Bot $VOXA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"reason": "спам ссылками"}'
{ "ok": true }
# снять бан — причина параметром, как у исключения
curl -s -G -X DELETE https://api.voxavoice.ru/api/bot/v1/servers/$SERVER/bans/$MEMBER \
-H "Authorization: Bot $VOXA_TOKEN" \
--data-urlencode "reason=договорились"curl -s https://api.voxavoice.ru/api/bot/v1/servers/$SERVER/bans \
-H "Authorization: Bot $VOXA_TOKEN"
[
{
"user": { "id": "a18e…", "username": "spammer", "display_name": "Спамер", "avatar": null },
"reason": "спам ссылками",
"banned_by": "3f6c9e1a-…",
"created_at": "2026-09-25T12:00:00Z",
"expires_at": null
}
]curl -s -X PUT https://api.voxavoice.ru/api/bot/v1/servers/$SERVER/members/$MEMBER/roles/$ROLE \
-H "Authorization: Bot $VOXA_TOKEN"
{ "ok": true, "role_ids": ["7b1c…", "e40a…"] }
# снять — DELETE по тому же адресу
curl -s -X DELETE https://api.voxavoice.ru/api/bot/v1/servers/$SERVER/members/$MEMBER/roles/$ROLE \
-H "Authorization: Bot $VOXA_TOKEN"
{ "ok": true, "role_ids": ["e40a…"] }# заглушить для всех и перенести в другой голосовой канал
curl -s -X PATCH https://api.voxavoice.ru/api/bot/v1/servers/$SERVER/members/$MEMBER/voice \
-H "Authorization: Bot $VOXA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"mute": true, "channel_id": "9c0d…", "reason": "шум в микрофоне"}'
{ "ok": true }
# отключить от голоса
curl -s -X PATCH https://api.voxavoice.ru/api/bot/v1/servers/$SERVER/members/$MEMBER/voice \
-H "Authorization: Bot $VOXA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel_id": null}'Управление сервером
С разрешениями channels.manage, roles.manage и server.manage бот создаёт и меняет каналы, их права и роли, а ещё название, иконку, баннер и описание сервера. Как и в модерации, нужны права роли: «Управление каналами», «Управление ролями», «Управление сервером».
| Метод и адрес | Разрешение | Тело и ответ |
|---|---|---|
PATCH /servers/{id} | server.manage | name, icon, banner, description — любые из них; "" убирает. Иконка и баннер — ссылка /uploads/… на свой загруженный файл (адрес из ответа загрузки подходит как есть; раздел «Файлы»). Ответ — сервер |
POST /servers/{id}/channels | channels.manage | name, kind (text или voice), topic, category_id. Ответ — канал |
PATCH /channels/{id} | channels.manage | name, topic, user_limit, is_news — любые. Ответ — канал |
DELETE /channels/{id} | channels.manage | Удалить канал вместе с его сообщениями |
PUT /channels/{id}/overwrites/{kind}/{target_id} | channels.manage | Права канала для одной роли (kind: role) или одного человека (user): тело {"allow", "deny"} — биты прав |
DELETE /channels/{id}/overwrites/{kind}/{target_id} | channels.manage | Убрать права канала для этой роли или человека |
POST /servers/{id}/roles | roles.manage | name, color, permissions, color2, gradient_anim. Ответ — роль |
PATCH /roles/{id} | roles.manage | Те же поля — любые из них. Ответ — роль |
DELETE /roles/{id} | roles.manage | Удалить роль |
- Права канала — по одной роли или одному человеку. Заменить их все разом бот не может: он видит только права для ролей и для себя, и замена целиком стёрла бы личные настройки людей, которых он не видит. Права в канале для другого человека — только тому, чья роль ниже роли бота, и не для владельца сервера. Выдать или снять можно лишь те права, что есть у бота в этом канале.
- Роли — только ниже своей. Новая роль встаёт в самый низ списка, её можно сразу выдавать. Свою роль и @everyone бот не меняет. Права роли не шире прав самого бота; права администратора не выдаются никогда.
- Каналы и роли бот видит в
GET /servers/{id}; у @everyone тамis_default: true— это$EVERYONEв примере ниже. - Категории и порядок каналов и ролей бот пока не меняет.
curl -s -X PATCH https://api.voxavoice.ru/api/bot/v1/servers/$SERVER \
-H "Authorization: Bot $VOXA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Клуб настолок", "description": "Играем по пятницам"}'
{ "id": "c47a0e55-…", "name": "Клуб настолок", "icon": null, "banner": null, "description": "Играем по пятницам", "owner_id": "91d2b7c4-…" }curl -s -X POST https://api.voxavoice.ru/api/bot/v1/servers/$SERVER/channels \
-H "Authorization: Bot $VOXA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "объявления", "kind": "text", "topic": "Только важное"}'# в «объявлениях» @everyone читает, но не пишет: 128 — «Отправка сообщений»
curl -s -X PUT https://api.voxavoice.ru/api/bot/v1/channels/$CHANNEL/overwrites/role/$EVERYONE \
-H "Authorization: Bot $VOXA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"allow": 0, "deny": 128}'
{ "ok": true }
# убрать это переопределение
curl -s -X DELETE https://api.voxavoice.ru/api/bot/v1/channels/$CHANNEL/overwrites/role/$EVERYONE \
-H "Authorization: Bot $VOXA_TOKEN"curl -s -X POST https://api.voxavoice.ru/api/bot/v1/servers/$SERVER/roles \
-H "Authorization: Bot $VOXA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Игрок", "color": "#3ba55d"}'
{ "id": "e40a…", "server_id": "c47a0e55-…", "name": "Игрок", "color": "#3ba55d", "permissions": 0, "position": 8, "is_default": false, … }Роль по кнопке
Отдельного механизма нет: это кнопка и выдача роли. Команда публикует сообщение с кнопками custom_id: "role:<id роли>". На нажатие бот сверяет id со своим списком и выдаёт роль (PUT …/roles/{role_id}), а если она уже есть — снимает (DELETE). Отвечает так, чтобы видел только нажавший.
- Нужны разрешение
roles.manage, право роли «Управление ролями» и роль бота выше тех ролей, которые он раздаёт. - Раздавайте роли без особых прав: «Читатель», «Игрок», цвет ника. Роль с правом, которого нет у бота, сервер выдать не даст.
- Кнопку и её
custom_idвидят все, кто видит сообщение, поэтому id роли всегда проверяйте по своему списку. - Есть ли роль у нажавшего, видно в
member.role_idsсобытия — отдельный запрос не нужен.
// тот же bot.mjs: «/роли» публикует кнопки, нажатие выдаёт или снимает роль
const ROLES = { "7b1c…": "Читатель", "e40a…": "Игрок" }; // СВОЙ список: ID — «Скопировать ID»
async function api(method, path, body) {
const r = await fetch(`${API}${path}`, {
method,
headers: { ...auth, ...(body ? { "Content-Type": "application/json" } : {}) },
body: body ? JSON.stringify(body) : undefined,
});
const data = await r.json();
if (!r.ok) throw new Error(data.error);
return data;
}
ws.on("message", async (raw) => {
const { t, d } = JSON.parse(raw.toString());
if (t !== "interaction_create") return;
const answer = (data) => api("POST", `/interactions/${d.id}/callback`, { token: d.token, type: "reply", data });
if (d.type === "command" && d.data.name === "роли") {
const buttons = Object.entries(ROLES).map(([id, label]) => ({ type: "button", label, custom_id: `role:${id}` }));
return answer({ content: "Нажмите, чтобы взять роль или снять её:", components: [{ type: "row", components: buttons }] });
}
if (d.type === "component" && d.data.custom_id.startsWith("role:")) {
const roleId = d.data.custom_id.slice("role:".length);
// custom_id видят все — сверяем со своим списком, а не верим кнопке
if (!ROLES[roleId] || !d.member) return answer({ content: "Эта кнопка больше не работает", ephemeral: true });
const has = d.member.role_ids.includes(roleId);
const path = `/servers/${d.server_id}/members/${d.user.id}/roles/${roleId}?reason=${encodeURIComponent("роль по кнопке")}`;
try {
await api(has ? "DELETE" : "PUT", path);
await answer({ content: has ? "Роль снята" : "Роль выдана", ephemeral: true });
} catch (e) {
// например, «можно выдавать и снимать только роли ниже вашей высшей»
await answer({ content: e.message, ephemeral: true });
}
}
});Файлы
Файл сначала загружают, потом прикрепляют к сообщению. Загрузка — POST /uploads: multipart/form-data с полем file, разрешение messages.write. В ответ приходит описание файла — положите его как есть в attachments при отправке (POST /channels/{id}/messages), до 10 файлов в сообщении. Сообщение может состоять из одних файлов.
curl -s -X POST https://api.voxavoice.ru/api/bot/v1/uploads \
-H "Authorization: Bot $VOXA_TOKEN" \
-F "file=@chart.png;type=image/png"
{
"url": "/uploads/5e1a9c….png?e=1790524800&s=q4…",
"name": "chart.png",
"size": 48213,
"mime": "image/png",
"width": 1200,
"height": 630,
"thumb": "/uploads/5e1a9c….t.jpg?e=1790524800&s=Hk…"
}- Правила те же, что у людей: те же запреты и та же проверка картинок. Файл — до 50 МБ, а пока аккаунту владельца бота меньше недели — до 10 МБ; подписка Voxa+ владельца пределы бота не расширяет. За сутки бот загружает не больше 2 ГБ и 300 файлов. Если владельцу запретили загружать файлы, его боты тоже не загрузят.
- Чтобы прикрепить файл, роли бота в канале нужно право «Прикрепление файлов». Прикрепить можно свой файл или тот, что уже есть вложением в канале, который бот видит.
- Файл, который так и не прикрепили, удаляется примерно через сутки.
- Отказы загрузки приходят 403 с текстом, который можно показать человеку: файл не прошёл автоматическую проверку (в тексте — код обращения); файл отправлен на проверку человеком и пока не опубликован — прикрепить его нельзя; владельцу запрещено загружать файлы.
- Правка сообщения файлы не меняет. В ответах на нажатия и команды файлов пока нет: ответьте на нажатие, а файл отправьте обычным сообщением.
- Адреса файлов временные. В ответе загрузки, в сообщениях (
GET /channels/{id}/messages) и в событиях шлюза адрес вложения выглядит как/uploads/<имя>?e=…&s=…и действует не дольше суток. Качайте файл по свежему адресу; сервер ответил 404 — перечитайте сообщение (GET /channels/{id}/messages?around=<seq>). Храните у себя id сообщения, а не адрес. Присылать подписанный адрес обратно можно — вattachmentsответ загрузки кладут как есть, в картинку карточки годится любая форма адреса файла: параметры сервер отбросит сам. Постоянные адреса без параметров — только у открытых картинок: аватары, значки и баннеры серверов, эмодзи и стикеры.
Своя картинка в карточке. Загрузите картинку, прикрепите её к этому же сообщению и сошлитесь в карточке на тот же url — в image, thumbnail, footer.icon_url или author.icon_url. Приложение покажет картинку в карточке и не станет повторять её среди вложений. Файл живёт, пока живо сообщение: удалили сообщение — удалится и файл. В сообщении картинка карточки приходит полным временным адресом (https://api.voxavoice.ru/uploads/…?e=…&s=…), а вложение — относительным; сравнивайте их по имени файла.
curl -s -X POST https://api.voxavoice.ru/api/bot/v1/channels/$CHANNEL/messages \
-H "Authorization: Bot $VOXA_TOKEN" \
-H "Content-Type: application/json" \
-d @- <<'EOF'
{
"content": "Продажи за неделю",
"attachments": [
{ "url": "/uploads/5e1a9c….png?e=1790524800&s=q4…", "name": "chart.png", "size": 48213,
"mime": "image/png", "width": 1200, "height": 630,
"thumb": "/uploads/5e1a9c….t.jpg?e=1790524800&s=Hk…" }
],
"embeds": [
{ "title": "Неделя 39", "image": { "url": "/uploads/5e1a9c….png?e=1790524800&s=q4…" }, "color": 3908957 }
]
}
EOFВебхуки
Вебхук — адрес, по которому другая программа публикует сообщения в канал: сборка, мониторинг, форма на сайте. Бот и токен для этого не нужны — хватит одного POST на адрес. Вебхуки бывают только у текстовых каналов серверов; исходящих вебхуков нет.
- Создать: настройки канала → вкладка «Вебхуки» → «Создать вебхук», имя и, если нужно, картинка. Нужно право «Управление вебхуками» в этом канале.
- Адрес показывается один раз, как токен бота, — скопируйте его сразу. Любой, у кого есть адрес, может писать в канал от имени вебхука: храните его в секретах, а не в коде. Если адрес утёк, нажмите «Сменить адрес» — старый перестанет работать сразу.
- Там же вебхук переименовывают и удаляют. Уже опубликованные сообщения остаются.
- В канале — до 10 вебхуков, на сервере — до 50. Создание, изменение и удаление видны в журнале аудита сервера.
curl -s -X POST "$VOXA_WEBHOOK" \
-H "Content-Type: application/json" \
-d '{"content": "Сборка №412 прошла"}'
{ "id": "8d2f…", "channel_id": "e5b81f07-…" }| Поле | Что это |
|---|---|
content | Текст, до 4000 символов |
embeds | Карточки — та же схема и те же пределы, что у бота (раздел «Карточки») |
username | Имя для этого сообщения, 1–32 символа, без слова Voxa. Нет поля — имя вебхука |
avatar_url | Картинка для этого сообщения — открытая картинка Voxa (/uploads/…: аватар, значок, эмодзи). Вложение сообщения сюда не подходит — вместо него покажется картинка вебхука. Нет поля — картинка вебхука |
curl -s -X POST "$VOXA_WEBHOOK" \
-H "Content-Type: application/json" \
-d @- <<'EOF'
{
"username": "Мониторинг",
"embeds": [
{
"title": "api.example.com не отвечает",
"description": "Три проверки подряд — **таймаут**",
"color": 15548997,
"fields": [ { "name": "С", "value": "12:04", "inline": true } ],
"timestamp": "2026-09-25T12:04:00Z"
}
]
}
EOF- Нужен
contentилиembeds. Кнопок и меню у вебхука нет — нажатие некому обработать; поле, которого нет в таблице, — ошибка 400. - Картинку автора с чужого сайта взять нельзя намеренно: приложение загружает её само, и чужой сайт узнавал бы по ней, кто и когда читает канал. В карточках внешние картинки можно — человек открывает их кнопкой «Показать».
- Картинки карточек у вебхука — https-адрес или открытая картинка Voxa. Своих файлов у вебхука нет, вложения сообщений не подходят (400).
- Сообщение подписано плашкой «ВЕБХУК». Имя и картинка — такие, какими были в момент отправки: переименование вебхука старые сообщения не меняет. Если картинку потом заменят или удалят, у старых сообщений она может пропасть.
- Исправить или удалить сообщение по адресу вебхука нельзя; удалить его могут модераторы канала, как любое сообщение.
- Неверный, сменённый и удалённый адрес отвечают одинаково: 404 «вебхук не найден — его удалили или сменили адрес».
- Пределы: 5 сообщений за 5 секунд на адрес, 30 в минуту на канал от всех вебхуков, 120 запросов в минуту на адреса вебхуков с одного IP. Тело запроса — до 256 КБ.
# GitHub Actions: адрес — в секретах репозитория, а не в файле
- name: Сообщить в Voxa
if: always()
run: |
curl -s -X POST "${{ secrets.VOXA_WEBHOOK }}" -H "Content-Type: application/json" \
-d "{\"content\": \"Сборка ${{ github.run_number }}: ${{ job.status }}\"}"Бот видит сообщение вебхука обычным message_create: у автора bot: true, а в meta.webhook — id, имя и картинка вебхука. Отвечать вебхукам, как и ботам, обычно не нужно.
{"t":"message_create","d":{"server_id":"c47a0e55-…","channel_kind":"text","message":{
"id": "8d2f…",
"seq": 1190,
"content": "Сборка №412 прошла",
"author": { "id": "0f3a…", "username": "webhook_0f3a…", "display_name": "Сборки", "avatar": null, "bot": true },
"meta": { "kind": "bot", "v": 1, "webhook": { "id": "2b9e41d0-…", "name": "Сборки", "avatar": null } },
…
}}}Дочитывание
Пока программа была без связи, события для неё не копились — пропущенное дочитывают историей. У каждого сообщения есть seq, и он общий для всех каналов: программе достаточно помнить одно число — самый большой seq из увиденного.
GET /channels/{id}/messages?after=<seq>&limit=100— сообщения сseqбольшеafter, по возрастанию. Вместе сbeforeилиaround— ошибка 400.- В кадре
readyестьresume.last_seq— последний номер на момент подключения. Всё, что не новее его, дочитывают историей; всё, что новее, придёт живыми событиями.null— сообщений ещё не было или отметку не удалось прочитать: тогда читайте историю до конца. - В
GET /servers/{id}естьlast_seqs— последнийseqкаждого видимого боту канала, где есть сообщения. Каналы, где ничего нового, можно не читать вовсе. - Личные:
GET /dmsдаётchannel_idиlast_seqразговоров; читать историю сafterнужно только там, гдеlast_seqбольше запомненного. - Если
resume.last_seqне больше запомненного, пропуска не было нигде — обходить ничего не нужно. - Одно и то же сообщение может прийти и живым событием, и в истории — отсекайте повторы по
id. - Правки и удаления, случившиеся без связи, не восстанавливаются.
# всё, что пришло в канал после seq 1043, по возрастанию
curl -s "https://api.voxavoice.ru/api/bot/v1/channels/$CHANNEL/messages?after=1043&limit=100" \
-H "Authorization: Bot $VOXA_TOKEN"// в ready — отметка: всё, что не новее last_seq, дочитывается историей
"resume": { "last_seq": 1187, "at": "2026-09-25T12:10:00Z" }
// в GET /servers/{id} — последний seq каждого видимого канала, где есть сообщения
"last_seqs": { "e5b81f07-…": 1187, "0c44…": 1102 }Порядок такой: подключились → получили ready → по каждому серверу GET /servers/{id} → каналы, где last_seqs больше запомненного, читаем страницами after до resume.last_seq → то же для личных, где last_seq больше запомненного. Не торопитесь: обход тратит то же окно чтения (120 в минуту), что и остальные запросы программы, и переподключение шлюза. Библиотека для JavaScript из следующего раздела делает всё это сама.
Библиотека для JavaScript
voxa.js — наша библиотека для Node.js и TypeScript, без зависимостей. Она сама подключается к шлюзу и переподключается, дочитывает пропущенное, отвечает на нажатия одной строкой, собирает кнопки, карточки и формы, загружает файлы и берёт на себя модерацию. Нужен Node 18 или новее; в Node 22+ всё встроено, в Node 18–21 нужен ещё пакет ws.
В каталоге npm её нет — она лежит архивом на сайте: voxa-js.tgz. Внутри — исходники на TypeScript, готовый JavaScript, подробный README на русском и шаблон бота: роль по кнопке, /kick, отчёт файлом и дочитывание после перезапуска.
npm i https://voxavoice.ru/files/voxa-js.tgz
# Node 18–21: ещё npm i ws и new VoxaClient({ token, WebSocket })// bot.mjs — запуск: VOXA_TOKEN=vxb_… node bot.mjs (Node 22+)
import { VoxaClient, ui } from "voxa.js";
const bot = new VoxaClient({ token: process.env.VOXA_TOKEN });
bot.on("message", async (msg, { replayed }) => {
if (msg.author.bot || msg.content.trim() !== "!ping") return;
await bot.send(msg.channel_id, { content: replayed ? "pong (с опозданием)" : "pong", reply_to: msg.id });
});
bot.on("interaction", async (ix) => {
if (ix.isCommand && ix.commandName === "кино") {
await ix.reply({
content: "Идём в пятницу?",
components: [ui.row(ui.button("Иду", "movie:yes", { style: "success" }))],
});
}
if (ix.customId === "movie:yes") await ix.reply("Записал!", { ephemeral: true });
});
await bot.commands.set([{ name: "кино", description: "Позвать в кино" }]);
await bot.connect();- События:
ready,message(у дочитанных —replayed: true),messageUpdate,messageDelete,interaction,memberJoin,memberLeave,disconnect,raw,error. - Ответы на нажатие:
reply,defer,update,deferUpdate,modal,editReply,deleteReply,followup— те же, что в разделе «Нажатия и ответы». - Помощники:
send,edit,upload,kick,ban,unban,addRole,removeRole,voice,createChannel,setOverwrite,createRole,editServerи другие; для остального —bot.rest.get,postи так далее. - Отметку дочитывания можно хранить между запусками:
new VoxaClient({ token, resume: { load, save } }). Дочитанное приходит по каналам — каждый канал по возрастаниюseq, как только дочитан. После первых десяти запросов обход идёт не чаще раза в секунду, чтобы оставить окно чтения программе. Сорвался (сбой сервера или сети) — библиотека повторит через 5 с, 30 с, 2 и 10 минут, не дожидаясь переподключения. - Коды 4003–4006 останавливают шлюз, 4007 — переподключение сразу, остальное — с паузой от 1 до 30 секунд. На 429 библиотека один раз ждёт
retry_atи повторяет запрос.
Голос
Бот с разрешением voice входит в голосовые каналы серверов и проигрывает там звук: музыку, звуковые эффекты, объявления. Людей бот не слышит — их голос ему не передаётся никогда. В личных, группах и звонках бота нет, видео и демонстрации экрана у него тоже нет. Звук бота идёт через сервер Voxa: напрямую с людьми бот не соединяется.
У людей бот в канале выглядит участником с плашкой «БОТ» и кольцом, когда звучит, а под именем — подпись, например «Играет: …». Каждый сам решает, насколько громко слышать бота, и может выключить его у себя. Модераторы заглушают, отключают и переносят бота теми же пунктами меню, что людей. Бота видно и слышно в новых версиях приложения, старые его не показывают.
Как включить. Отметьте у бота «Играть звук в голосовых каналах» (voice). Войти в канал бот может, если у его роли там есть «Видеть канал» и «Подключение к голосу»; чтобы его было слышно — ещё «Говорить». У @everyone эти права по умолчанию есть. Без «Говорить» бот входит, но молчит: придёт muted с причиной no_speak.
Подключение. Голос — отдельный WebSocket: wss://api.voxavoice.ru/api/bot/v1/voice. Токен — только заголовком Authorization: Bot vxb_…: адрес с ?token= здесь не принимается. У сокета не больше одной сессии, то есть одного сервера; чтобы играть на нескольких серверах, откройте по сокету на каждый. Отказы приходят до подключения, обычным HTTP с телом {"error": "…"}:
| Код | Когда |
|---|---|
| 401 | Нет заголовка Authorization: Bot … или токен не принят |
| 403 | У токена нет разрешения voice; бот отключён или заблокирован; «Голос для ботов выключен администрацией Voxa» — это не навсегда: пробуйте снова раз в 10 минут |
| 429 | У бота уже 12 голосовых сокетов — ждите до retry_at |
| 503 | Сервер Voxa сейчас занят — повторите с растущей паузой |
| 400 | Обычный запрос вместо WebSocket |
Сразу после подключения сервер присылает hello с числами протокола. Программа шлёт join с ID голосового канала, сервер отвечает joined — или error с кодом и текстом. Сессия живёт, пока программа не пришлёт leave, бота не отключат или не пропадёт повод быть в канале; сокет после этого остаётся, можно снова join. join в другой канал того же сервера — переход той же сессии.
← {"t":"hello","d":{"version":1,"heartbeat_secs":30,"packet_ms":[10,20,40,60],"max_packets":5,"max_packet_bytes":1275,"max_message_bytes":4096,"max_messages_per_sec":25,"lead_ms":400,"bitrate_slack":1.15,"bitrate_burst_secs":4}}
→ {"t":"join","d":{"channel_id":"5d1e…"}}
← {"t":"joined","d":{"server_id":"c47a0e55-…","channel_id":"5d1e…","session":17,"bitrate":48000,"humans":3,"hearing":2,"muted":false,"muted_reason":null,"resumed":false}}
→ {"t":"caption","d":{"text":"Играет: Лунная соната"}}
→ (бинарь) пачка звука — примерно раз в 60 мс
← {"t":"listeners","d":{"humans":4,"hearing":3}}
← {"t":"muted","d":{"on":true,"reason":"moderator"}}
← {"t":"muted","d":{"on":false,"reason":null}}
→ {"t":"leave","d":{}}
← {"t":"left","d":{"reason":"self","channel_id":"5d1e…"}}| Кадр | Что значит |
|---|---|
hello | Числа протокола: длительности пакета, пределы пачки, рекомендованное опережение lead_ms, запас битрейта |
joined | Бот в канале. bitrate — потолок звука этой сессии в бит/с; humans — людей в канале; hearing — сколько из них сейчас слышат бота (кто именно — бот не узнаёт); resumed: true — продолжение прежней сессии после обрыва |
listeners | humans и hearing изменились — не чаще раза в секунду |
muted | Бота заглушили для всех (on: true, reason: moderator или no_speak — у роли нет права «Говорить») или вернули звук. Звук заглушённого бота сервер никому не отдаёт — ставьте дорожку на паузу |
moved | Модератор перенёс бота в другой канал того же сервера — играйте дальше |
bitrate | Уровень сервера сменился; новый потолок — со следующего join |
left | Сессия кончилась. reason: self, kicked (с retry_after_secs), removed — бота убрали с сервера, no_access — роль потеряла доступ к каналу, channel_deleted, server_deleted, alone — людей нет 5 минут, replaced — бот вошёл в голос этого сервера с другого сокета, admin, disabled — голос для ботов выключен |
slow_down | Сервер выбросил звук: reason — too_fast, bitrate или too_many_messages; не чаще раза в 2 секунды |
bad_packet | Сервер выбросил пачку с неправильными пакетами: reason — batch, toc, structure или duration; не чаще раза в 5 секунд |
error | Отказ на кадр программы: code, message — текст для людей, retry_after_secs |
pong | Ответ на ping |
| Кадр | Что делает |
|---|---|
{"t":"join","d":{"channel_id":"…"}} | Войти в голосовой канал; тот же сервер — перейти, другой сервер — прежняя сессия сокета кончается |
{"t":"leave","d":{}} | Выйти из голоса |
{"t":"caption","d":{"text":"…"}} | Подпись под ботом: одна строка до 80 символов; null — убрать |
{"t":"ping","d":{}} | Признак жизни: без единого кадра 75 секунд сокет закрывается с кодом 4008 |
| code | Когда |
|---|---|
forbidden | У роли бота нет «Видеть канал» или «Подключение к голосу» в этом канале, либо бота нет на сервере |
not_found, not_voice | Канала нет; это не голосовой канал сервера |
disabled | Голос для ботов выключен администрацией Voxa |
limit_channel, limit_server, limit_bot | В канале уже два бота; на сервере уже играют три; бот уже играет на десяти серверах |
busy | Сервер Voxa сейчас занят — попробуйте позже |
kicked_recently | Бота отключили от голоса на этом сервере: вернуться можно через retry_after_secs |
rate_limited | Слишком часто: больше 10 входов в минуту на сервере или больше 10 текстовых кадров в секунду |
not_joined | Звук или подпись без сессии — сначала join |
bad_request | Незнакомый кадр или подпись длиннее 80 символов |
| Код | Что случилось |
|---|---|
| 4003 | Бота отключила администрация |
| 4004 | Выпущен новый токен или токен отозван |
| 4005 | Бот удалён |
| 4006 | Бот или его владелец заблокирован |
| 4007 | Владелец поменял разрешения бота — переподключитесь сразу; без voice сокет больше не откроется |
| 4008 | 75 секунд без единого кадра от программы |
| 4009 | Сбой на нашей стороне — переподключитесь с паузой |
| 1009 | Бинарь больше 4096 байт или текст больше 2048 |
Перед закрытием приходит кадр disconnect с тем же code и причиной. Оборвалась связь — переподключитесь и пришлите join в тот же канал: успели за 15 секунд — сервер продолжит ту же сессию (resumed: true), и люди не увидят, что бот выходил.
Звук. Бинарное сообщение — пачка пакетов Opus. Номера пакетам ставит сервер, программа их не шлёт: после перезапуска просто продолжайте.
смещение байт что
0 1 версия = 1
1 1 N — пакетов в пачке, от 1 до 5
2 … N раз: длина пакета (2 байта, little-endian, 1–1275) и сам пакет Opus
всё сообщение — до 4096 байт, после N-го пакета — ни байта- Opus 48 кГц, моно или стерео. Пакет длится 10, 20, 40 или 60 мс (лучше 20) и весит не больше 1275 байт. Каждый пакет сервер проверяет по RFC 6716: сломанный пакет ронял бы декодеры слушателей.
- В пачке — от 1 до 5 пакетов, лучше 3 по 20 мс и не меньше 40 мс звука; всё сообщение — до 4096 байт. Больше 25 сообщений в секунду сервер не примет (
too_many_messages). - Темп — реальное время. Держите у сервера не больше 400 мс звука впереди, лучше 200: считайте время от старта и шлите пачку, когда её звук кончается не дальше этого запаса. Быстрее — сервер выбросит лишнее (
too_fast). - Громкость — около −16 LUFS. Резкий или намеренно громкий звук, который мешает разговору, запрещён Условиями для разработчиков.
| Уровень сервера | Потолок звука бота |
|---|---|
| 0 | 32 кбит/с |
| 1 | 48 кбит/с |
| 2 | 64 кбит/с |
| 3 и официальный сервер Voxa | 96 кбит/с |
Потолок приходит в joined.bitrate и не меняется до конца сессии. Сервер пропускает до 1,15 × потолка и всплески до 4 секунд потолка, тяжелее — выбрасывает (bitrate). Причины bad_packet: batch — сама пачка не разбирается; toc — пакет пустой или его заголовок описывает невозможный пакет; structure — длины кадров не сходятся с размером пакета; duration — пакет правильный, но длится не 10, 20, 40 или 60 мс. dropped в slow_down и bad_packet — сколько сообщений выброшено с прошлого отчёта. У правильной программы этих кадров не бывает.
Где взять Opus. Любой файл пережимает ffmpeg: пакеты по 20 мс, битрейт не выше потолка сервера. Права на музыку — на владельце бота: играйте только своё или то, на что у вас есть разрешение правообладателя (Условия для разработчиков).
ffmpeg -i песня.mp3 -c:a libopus -b:a 48k -vbr constrained -frame_duration 20 песня.oggКуда заходить. Бот с разрешением voice получает в interaction_create поле member.voice_channel_id — голосовой канал на этом сервере того, кто вызвал команду или нажал кнопку. null — человек не в голосе или роль бота этот канал не видит; у ботов без voice поля нет вовсе. Узнать, где сидит человек, не дождавшись его команды, бот не может.
"member": {
"nickname": null,
"role_ids": ["7a3d…"],
"permissions": 16512,
"voice_channel_id": "5d1e…"
}- В одном канале — до 2 ботов, на сервере — до 3; один бот играет одновременно не больше чем на 10 серверах, голосовых сокетов у бота — до 12.
- Входов (
join) — до 10 в минуту на каждом сервере; текстовых кадров — до 10 в секунду. - Отключили от канала — вернуться можно через минуту, при повторе в течение часа — через 10 минут, дальше — через час.
- Бот один в канале дольше 5 минут — сервер выводит его (
leftс причинойalone). - Подпись — одна строка до 80 символов и не чаще раза в 5 секунд: если чаще, применится последняя.
Пример на Node 24 без пакетов: играет один файл .ogg в канале CHANNEL, ставит дорожку на паузу, пока бот заглушён, и выходит, когда файл кончился.
// music.mjs — запуск: VOXA_TOKEN=vxb_… CHANNEL=<ID голосового канала> node music.mjs песня.ogg
// Node 24, без пакетов: WebSocket с заголовками встроен.
import { readFileSync } from "node:fs";
import { basename } from "node:path";
const LEAD_MS = 200; // держим у сервера не больше 200 мс звука впереди
// 1. Ogg → пакеты Opus: страницы, lacing (255 — «продолжение следует»).
function opusPackets(file) {
const b = readFileSync(file);
const out = [];
let part = [];
for (let pos = 0; pos < b.length; ) {
if (b.toString("latin1", pos, pos + 4) !== "OggS") throw new Error("это не Ogg");
const n = b[pos + 26];
let off = pos + 27 + n;
for (const len of b.subarray(pos + 27, pos + 27 + n)) {
part.push(b.subarray(off, off + len));
off += len;
if (len < 255) { out.push(Buffer.concat(part)); part = []; }
}
pos = off;
}
return out.slice(2); // без OpusHead и OpusTags
}
// Длительность пакета по его первому байту (RFC 6716), мс.
function packetMs(p) {
const c = p[0] >> 3;
const frame = c < 12 ? [10, 20, 40, 60][c % 4] : c < 16 ? [10, 20][c % 2] : [2.5, 5, 10, 20][c % 4];
const code = p[0] & 3;
return frame * (code === 0 ? 1 : code === 3 ? p[1] & 0x3f : 2);
}
// 2. Пачка: версия 1, число пакетов, у каждого — длина (2 байта LE) и сам пакет.
function batch(list) {
const buf = Buffer.alloc(2 + list.reduce((s, p) => s + 2 + p.length, 0));
buf[0] = 1;
buf[1] = list.length;
let o = 2;
for (const p of list) { buf.writeUInt16LE(p.length, o); p.copy(buf, o + 2); o += 2 + p.length; }
return buf;
}
const packets = opusPackets(process.argv[2]);
const ws = new WebSocket("wss://api.voxavoice.ru/api/bot/v1/voice", {
headers: { Authorization: "Bot " + process.env.VOXA_TOKEN }, // только заголовком
});
const send = (t, d = {}) => ws.send(JSON.stringify({ t, d }));
let pos = 0; // следующий неотправленный пакет: с него продолжаем после паузы
let timer = null;
let done = false;
// 3. Темп — по звуковым часам от старта: ошибки таймера не копятся.
function play() {
const start = Date.now();
let sent = 0; // мс звука, отправленного с этого старта
const tick = () => {
while (pos < packets.length) {
const list = packets.slice(pos, pos + 3); // 3 × 20 мс
const ms = list.reduce((s, p) => s + packetMs(p), 0);
if (start + sent + ms - LEAD_MS > Date.now()) break;
ws.send(batch(list));
pos += list.length;
sent += ms;
}
// последний звук ещё впереди часов и в запасе у слушателей — уйти, когда доиграет
if (pos >= packets.length) {
timer = setTimeout(() => send("leave"), Math.max(0, start + sent - Date.now()) + 500);
return;
}
timer = setTimeout(tick, 20);
};
tick();
}
const pause = () => clearTimeout(timer);
ws.addEventListener("open", () => send("join", { channel_id: process.env.CHANNEL }));
ws.addEventListener("message", (e) => {
const { t, d } = JSON.parse(e.data);
if (t === "joined") {
console.log("в канале: потолок " + d.bitrate / 1000 + " кбит/с, слышат " + d.hearing + " из " + d.humans);
send("caption", { text: ("Играет: " + basename(process.argv[2], ".ogg")).slice(0, 80) });
if (!d.muted) play();
}
if (t === "muted") d.on ? pause() : play(); // заглушили для всех — пауза, вернули — дальше
if (t === "left") { done = true; console.log("вышел: " + d.reason); ws.close(); }
if (t === "error" || t === "slow_down" || t === "bad_packet") console.warn(t, d);
});
const ping = setInterval(() => send("ping"), 30_000); // иначе через 75 с тишины — 4008
ws.addEventListener("close", (e) => {
pause();
clearInterval(ping);
if (!done) console.warn("соединение закрыто: " + e.code); // 4003–4009 — таблица выше
});Как узнать ID
У каждого человека, бота, сервера, канала, сообщения и роли в Voxa есть постоянный ID. Имя и @логин можно сменить, ID — никогда, поэтому бот «только для своих» надёжнее всего проверяет именно его.
- В приложении Voxa откройте «Настройки» → «Боты» и в разделе «Для разработчиков» включите Режим разработчика.
- Нажмите правой кнопкой (на телефоне — задержите палец) на человека, сервер, канал или сообщение и выберите «Скопировать ID». На телефоне ID сервера — в меню сервера: коснитесь его названия над списком каналов.
- ID роли — тем же жестом на её теге в карточке человека или на строке во вкладке «Роли» настроек сервера. У @everyone ID тоже есть, но в
role_idsего не бывает: «любой участник сервера» проверяйте поserver_id. ID своего бота — кнопка «ID» в его карточке во вкладке «Боты».
| Что | В `message_create` | В `interaction_create` |
|---|---|---|
| Человек | d.message.author.id | d.user.id |
| Сервер | d.server_id — null в личных | d.server_id — null в личных |
| Канал | d.message.channel_id | d.channel_id |
| Роли человека | — | d.member.role_ids — member: null в личных |
| Сообщение | d.message.id | d.message.id — у нажатия кнопки |
Отвечать только своим и только на своём сервере — сравните ID в начале обработчика. Ролей в message_create нет; кому можно команду, решайте по member.role_ids в interaction_create, а отказ отправьте так, чтобы видел только нажавший:
// ID — из приложения: включите режим разработчика и нажмите «Скопировать ID»
const HOME_SERVER = "c47a0e55-…"; // где бот работает
const FRIENDS = new Set(["91d2b7c4-…", "a18e…"]); // кому он отвечает
const DJ_ROLE = "7a3d…"; // кому можно команду /музыка
ws.on("message", async (raw) => {
const { t, d } = JSON.parse(raw.toString());
if (t === "message_create") {
if (d.server_id !== HOME_SERVER) return; // другой сервер или личные (null)
if (!FRIENDS.has(d.message.author.id)) return; // не из списка — молчим
// …дальше как в примере «Шлюз событий»
}
if (t === "interaction_create" && d.type === "command" && d.data.name === "музыка") {
// member есть только на сервере; в личных он null
const allowed = d.server_id === HOME_SERVER && d.member?.role_ids.includes(DJ_ROLE);
const answer = allowed
? { type: "reply", data: { content: "Включаю" } }
: { type: "reply", data: { content: "Эта команда — только для роли DJ", ephemeral: true } };
await fetch(`${API}/interactions/${d.id}/callback`, {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({ token: d.token, ...answer }),
});
}
});Пределы
| Что | Сколько |
|---|---|
Чтения (GET, включая подключение к шлюзу) | 120 в минуту на бота |
Записи (POST, PUT, PATCH, DELETE), кроме ответов на нажатия, — в том числе загрузки, модерация и управление | 30 в минуту на бота |
| Сообщения в один канал | 5 за 5 секунд |
| Подключения к шлюзу | 5 одновременно; шестое вытесняет самое старое |
| Длина сообщения | 4000 символов |
| Ботов на одном сервере | 20 |
| Ботов у одного человека | 5 |
Новые личные разговоры, которые бот открывает сам (POST /dms) | 30 в час |
| Сообщения в личные, где человек молчит больше 24 часов | 60 в час на бота и 10 в сутки одному человеку |
Ответы на нажатия (/interactions/…) | 600 в минуту на бота, отдельно от записей |
| Первый ответ на нажатие | 3 секунды |
Токен ответа, PATCH и DELETE …/original, followup | 15 минут; дополнительных ответов — до 5 |
Перерегистрация команд (PUT) | 20 в час; тот же самый список не считается |
| Команды | до 100 в списке, у команды до 25 опций, у опции до 25 вариантов |
| Кнопки и меню | до 5 рядов, в ряду до 5 кнопок или одно меню, в сообщении до 25 |
| Карточки | до 10 в сообщении, текста в них — до 6000 символов |
| Нажатия одного человека | 30 в минуту |
| Модерация: исключения, баны, роли участника, голос | 20 в минуту на бота на каждом сервере |
| Изменения сервера: каналы, их права, роли, настройки | 10 в минуту на бота на каждом сервере |
| Файл | до 50 МБ, пока аккаунту владельца меньше недели — до 10 МБ; за сутки — 2 ГБ и 300 файлов на бота |
| Файлы в сообщении | до 10 |
| Вебхуки | до 10 в канале и до 50 на сервере |
| Сообщения по адресу вебхука | 5 за 5 секунд на адрес, 30 в минуту на канал; 120 запросов в минуту с одного IP |
| Голос: ботов в канале и на сервере | 2 и 3; один бот — не больше чем на 10 серверах сразу |
| Голос: сокеты и входы | 12 голосовых сокетов у бота; 10 входов (join) в минуту на каждом сервере |
| Голос: звук | до 25 сообщений в секунду; потолок битрейта — по уровню сервера, раздел «Голос» |
Сверх предела — ответ 429 и время, когда можно продолжить. Ждите до retry_at, а не повторяйте запрос сразу:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{ "error": "слишком много запросов бота, подождите 12 с", "retry_at": "2026-09-24T12:00:12Z" }Ошибки
Ошибка — HTTP-код и тело {"error": "текст"}. Текст написан для человека и может меняться: программе решать по коду.
| Код | Когда |
|---|---|
| 400 | Неверный запрос: пустое или слишком длинное сообщение, поле, которого нет в описании, before, around и after вместе; в голосе нечего менять или канал назначения — не голосовой канал этого сервера; неверные кнопки, карточки, форма или команды — в тексте указано место; ответ не того типа: update не к нажатию, форма на форму, правка или дополнительный ответ после modal (отвечайте на отправку формы) |
| 401 | Нет токена, токен неверный или отозван, схема не Bot |
| 403 | У токена нет нужного разрешения или у роли бота — нужного права; файл не прошёл проверку или отправлен на проверку (раздел «Файлы»); роль участника или выдаваемая роль не ниже роли бота; канал закрыт для бота; личный разговор с тем, кто не разрешал боту писать, или группа; блокировка; бот отключён |
| 404 | Сервера, канала, сообщения или роли нет; интеракция не найдена или её токен устарел; адрес вебхука неверный, сменён или вебхук удалён |
| 409 | На интеракцию уже ответили или первый ответ опоздал (3 секунды); сначала нужен первый ответ; исходного ответа ещё нет (после defer) или его уже удалили; участник не в голосовом канале этого сервера |
| 429 | Превышен предел — ждите до retry_at; у «не больше 5 дополнительных ответов» retry_at нет |
| 5xx | Сбой на нашей стороне — повторите позже, с паузой |
| Голос | Отказы голосового сокета до подключения, кадры error, slow_down, bad_packet и коды закрытия — в разделе «Голос» |
Упоминания и хорошие манеры
Упоминание в Voxa — это просто текст @имя в сообщении. Чтобы понять, что обратились к вашему боту, ищите в content его @имя — без учёта регистра и как отдельное слово. Чтобы позвать человека, напишите в ответе его @username. @everyone в сообщении бота остаётся текстом: уведомлений всем участникам бот не рассылает.
- Не отвечайте ботам. У сообщения бота
author.botравноtrue— пропускайте такие, иначе два бота заведут бесконечный разговор. Свои сообщения тоже пропускайте. - Отвечайте на команды и упоминания, а не на всё подряд. Бот, который комментирует каждое сообщение, — это спам.
- Отвечайте ответом (
reply_to), чтобы было видно, на что именно. - Не упоминайте людей без повода и не пишите туда, где бота не спрашивали.
- Отвечайте на нажатие сразу. Нужно время —
defer, а не молчание: иначе человек увидит «Бот не ответил». - Подтверждения, ошибки и личные списки — «видно только вам»: канал не засоряется.
- Регистрируйте команды при каждом запуске — тот же список сервер не перезаписывает.
- Напишите в описании бота, что он делает и хранит ли что-нибудь.
- Храните только то, без чего бот не работает, и удаляйте, когда бота убрали с сервера или человек запретил писать ему в личные.
Что нельзя и за что отключают
Подробно — в Условиях для разработчиков. Коротко: за бота отвечает его владелец, и правила для бота те же, что для людей. Нельзя:
- выдавать бота за человека, за администрацию Voxa или за официального бота;
- спамить: массовые и повторяющиеся сообщения, реклама без согласия владельца сервера;
- проигрывать в голосе звук без прав на него, мешать разговору резким или намеренно громким звуком, выдавать звук бота за голос конкретного человека;
- копить сообщения и списки участников «про запас», продавать и передавать данные людей;
- выманивать пароли, коды и токены — в том числе кнопками и формами, спрашивать в них платёжные данные;
- выманивать разрешение писать в личные обманом или в обмен на что-либо; писать в личные рекламу и рассылки, о которых человек не просил;
- обходить пределы — несколькими ботами или копиями программы — и блокировки, например заводить нового бота вместо отключённого;
- массово исключать, банить и удалять каналы без решения администрации сервера;
- искать и использовать уязвимости. Нашли — напишите на legal@voxavoice.ru.
За нарушение администрация отключает бота: программа теряет доступ сразу (код закрытия 4003), владелец получает уведомление с причиной. Отключение можно обжаловать через обращение в поддержку.
Документы
- Условия для разработчиков — их принимают при создании бота.
- Политика конфиденциальности — раздел 7: что получают владельцы ботов.
- Правила сообщества — действуют и для ботов.
Вопросы по API — help@voxavoice.ru.