— разработчикам

API для ботов

Бот — это аккаунт Voxa, которым управляет ваша программа. Здесь всё, что нужно, чтобы его написать: как получить токен, какие есть запросы, как слушать события и где пределы.

Что такое бот

Бот читает и пишет в текстовых каналах серверов, куда его добавили, ставит реакции, показывает кнопки, меню, карточки и формы, отвечает на команды через «/» и узнаёт о событиях на сервере — так же, как участник с теми же правами. Если разрешили владелец бота и сервер, бот ещё и модерирует: исключает, банит, выдаёт роли, управляет каналами и настройками сервера. Рядом с именем бота всегда стоит плашка «БОТ», а в его карточке видно владельца.

В личные сообщения бот пишет только тем, кто сам ему это разрешил, — подробно в разделе «Личные сообщения». Бот не бывает в группах и звонках, не заводит друзей и не вступает на серверы по приглашениям; в голосовых каналах серверов он может проигрывать звук — раздел «Голос». На сервер его добавляет человек с правом управлять этим сервером.

Это первая версия 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.

Запросы

Запросы API
Метод и адресРазрешениеЧто делает
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}/messagesmessages.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}/reactionsmessages.readРеакции и первые 30 поставивших каждую
POST /messages/{id}/reactionsreactionsПоставить реакцию: emoji (символ или :имя: эмодзи сервера) или emoji_id
DELETE /messages/{id}/reactions?emoji= или ?emoji_id=reactionsСнять свою реакцию
POST /channels/{id}/typingmessages.writeПоказать «печатает» в канале
GET /gateway—Шлюз событий по WebSocket — следующий раздел
GET /voicevoiceГолосовой сокет по WebSocket: войти в голосовой канал и проигрывать звук — раздел «Голос»
POST /dms, GET /dmsmessages.write, messages.readОткрыть личный разговор и узнать, кто разрешил писать, — раздел «Личные сообщения»
GET, PUT /commands; GET, PUT /servers/{id}/commands; DELETE /commands/{id}—Команды через «/» — раздел «Команды»
POST /interactions/{id}/callback, PATCH и DELETE …/original, POST …/followupmessages.writeОтветы на нажатия, команды и формы — раздел «Нажатия и ответы»
POST /uploadsmessages.writeЗагрузить файл — раздел «Файлы»
…/members/{user_id}, …/bans/…, …/roles/{role_id}, …/voicemembers.manage, roles.manageИсключения, баны, роли участника, голос — раздел «Модерация»
PATCH /servers/{id}, …/channels, …/overwrites/…, …/rolesserver.manage, channels.manage, roles.manageСервер, каналы, их права и роли — раздел «Управление сервером»

В примерах ответы сокращены: у объектов бывают и другие поля (например, оформление профиля у user), а новые могут появиться. Не рассчитывайте на их отсутствие и не падайте на незнакомых.

Кто я. Бот, его владелец, разрешения и адрес шлюза:

GET /me
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 }
}

Серверы бота и один сервер целиком — только то, что боту видно:

GET /servers
curl -s https://api.voxavoice.ru/api/bot/v1/servers \
  -H "Authorization: Bot $VOXA_TOKEN"

[
  { "id": "c47a0e55-…", "name": "Дом", "icon": null, "owner_id": "91d2b7c4-…" }
]
GET /servers/{id}
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:

GET /servers/{id}/members
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.

GET /channels/{id}/messages
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 иначе молча ушла бы сообщением без карточки.

POST /channels/{id}/messages
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.

PATCH /messages/{id}
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, облачно"}'
DELETE /messages/{id}
curl -s -X DELETE https://api.voxavoice.ru/api/bot/v1/messages/$MESSAGE \
  -H "Authorization: Bot $VOXA_TOKEN"

{ "ok": true }

Реакции. Эмодзи в адресе снятия реакции кодируется как часть адреса:

POST /messages/{id}/reactions
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 }
DELETE /messages/{id}/reactions
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 }
GET /messages/{id}/reactions
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 сейчас не помещается в места по уровню сервера: его :имя: в тексте остаётся буквами, а новую реакцию им не поставить — поддержать уже стоящую можно.

POST /messages/{id}/reactions — свой эмодзи
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 }

«Печатает». Показывается недолго; если ответ готовится дольше, повторяйте.

POST /channels/{id}/typing
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)
websocat -H "Authorization: Bot $VOXA_TOKEN" \
  wss://api.voxavoice.ru/api/bot/v1/gateway

Каждый кадр — JSON {"t": тип, "d": данные}. Первым приходит ready:

Кадр 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.

ping и pong
→ {"t":"ping"}
← {"t":"pong","d":{}}
Коды закрытия
КодПочемуЧто делать
4003Бот отключён администрациейНе переподключаться; владелец получил уведомление с причиной
4004Токен заменён или отозванНе переподключаться с этим токеном
4005Бот удалёнОстановиться
4006Бот или его владелец заблокированОстановиться
4007Владелец изменил разрешения ботаПереподключиться сразу и перечитать GET /me
400875 секунд тишиныПереподключиться
4009 или обрыв без кадраБот не успевал читать события, шестое подключение вытеснило старое или сервер перезапустилсяПереподключиться с паузой и дочитать историю

После переподключения пропущенные события не приходят заново — дочитайте их историей с after=<последний seq>, как описано в разделе «Дочитывание». Переподключайтесь с растущей паузой — от секунды до минуты, со случайным разбросом, чтобы не бить в сервер все разом.

Бот на Node.js — отвечает pong на !ping:

bot.mjs
// 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 /dmsmessages.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 — сообщений нет)
POST /dms
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
}
GET /dms
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; обязательные — раньше необязательных
Поля опции
ПолеЧто это
typestring, 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
commands.json
[
  {
    "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 }
    ]
  }
]
PUT /commands
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_id1–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
footertext 1–2048 и icon_url
authorname 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.

Кадр interaction_create (команда)
{"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"
}}
data у нажатия и у формы
// нажатие кнопки или выбор в меню — "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, … }, … } }

Цикл «нажали → обновили». Человек нажал кнопку под сообщением — бот правит это же сообщение и отдельно отвечает только ему:

update и followup
# человек нажал «Иду» под сообщением $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}'
defer и PATCH original
# сначала «думаю» — у человека строка «Погода думает…»
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, дождь после обеда"}'
DELETE original
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 — ответы
// тот же 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_id1–100 символов — придёт с отправкой
titleЗаголовок, 1–45 символов
fieldsОт 1 до 5 полей ввода
fields[].custom_id, label1–100 (уникален в форме) и подпись 1–45
fields[].styleshort — строка, paragraph — абзац; по умолчанию short
fields[].requiredОбязательное ли; по умолчанию true
fields[].placeholderПодсказка в пустом поле, до 100 символов
fields[].min_length, max_length0–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}/bansmembers.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}/voicemembers.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=договорились"
GET /servers/{id}/bans
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…"] }
PATCH …/voice
# заглушить для всех и перенести в другой голосовой канал
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.managename, icon, banner, description — любые из них; "" убирает. Иконка и баннер — ссылка /uploads/… на свой загруженный файл (адрес из ответа загрузки подходит как есть; раздел «Файлы»). Ответ — сервер
POST /servers/{id}/channelschannels.managename, kind (text или voice), topic, category_id. Ответ — канал
PATCH /channels/{id}channels.managename, 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}/rolesroles.managename, color, permissions, color2, gradient_anim. Ответ — роль
PATCH /roles/{id}roles.manageТе же поля — любые из них. Ответ — роль
DELETE /roles/{id}roles.manageУдалить роль
  • Права канала — по одной роли или одному человеку. Заменить их все разом бот не может: он видит только права для ролей и для себя, и замена целиком стёрла бы личные настройки людей, которых он не видит. Права в канале для другого человека — только тому, чья роль ниже роли бота, и не для владельца сервера. Выдать или снять можно лишь те права, что есть у бота в этом канале.
  • Роли — только ниже своей. Новая роль встаёт в самый низ списка, её можно сразу выдавать. Свою роль и @everyone бот не меняет. Права роли не шире прав самого бота; права администратора не выдаются никогда.
  • Каналы и роли бот видит в GET /servers/{id}; у @everyone там is_default: true — это $EVERYONE в примере ниже.
  • Категории и порядок каналов и ролей бот пока не меняет.
PATCH /servers/{id}
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-…" }
POST /servers/{id}/channels
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"
POST /servers/{id}/roles
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 — роль по кнопке
// тот же 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 файлов в сообщении. Сообщение может состоять из одних файлов.

POST /uploads
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
# всё, что пришло в канал после seq 1043, по возрастанию
curl -s "https://api.voxavoice.ru/api/bot/v1/channels/$CHANNEL/messages?after=1043&limit=100" \
  -H "Authorization: Bot $VOXA_TOKEN"
resume и last_seqs
// в 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.js
// 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 — продолжение прежней сессии после обрыва
listenershumans и 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
Коды error
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 сокет больше не откроется
400875 секунд без единого кадра от программы
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. Резкий или намеренно громкий звук, который мешает разговору, запрещён Условиями для разработчиков.
Потолок битрейта по уровню сервера
Уровень сервераПотолок звука бота
032 кбит/с
148 кбит/с
264 кбит/с
3 и официальный сервер Voxa96 кбит/с

Потолок приходит в 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 поля нет вовсе. Узнать, где сидит человек, не дождавшись его команды, бот не может.

interaction_create — member
"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
// 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» в его карточке во вкладке «Боты».
Где ID лежат в событиях
ЧтоВ `message_create`В `interaction_create`
Человекd.message.author.idd.user.id
Серверd.server_id — null в личныхd.server_id — null в личных
Каналd.message.channel_idd.channel_id
Роли человека—d.member.role_ids — member: null в личных
Сообщениеd.message.idd.message.id — у нажатия кнопки

Отвечать только своим и только на своём сервере — сравните ID в начале обработчика. Ролей в message_create нет; кому можно команду, решайте по member.role_ids в interaction_create, а отказ отправьте так, чтобы видел только нажавший:

bot.mjs — только для своих
// 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 }),
    });
  }
});

Пределы

Пределы API
ЧтоСколько
Чтения (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, followup15 минут; дополнительных ответов — до 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, а не повторяйте запрос сразу:

Ответ 429
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), владелец получает уведомление с причиной. Отключение можно обжаловать через обращение в поддержку.

Документы

Вопросы по API — help@voxavoice.ru.