Что такое бот и зачем он нужен
Бот — это ещё один вид «учётной записи» в Korat, за которым никто не сидит: запросы отправляет ваша собственная программа, а сообщение появляется в том диалоге, куда бота пригласили. Реальные применения:
- Сообщать о новом заказе в чат — ваша торговая система получила заказ ⇒ бот тут же пишет об этом в диалог с клиентом или в комнату команды
- Предупреждать, что товар заканчивается — остаток опустился ниже заданного ⇒ бот пишет в комнату команды
- Сообщать о состоянии серверов и систем — ваш скрипт проверки отработал ⇒ бот отчитывается в общую комнату, за которой следят технари
Чего бот не умеет (это факты из базы данных, а не временные ограничения):
- Бот не читает сообщения в диалоге — он работает только на выход, если только вы сами не настроили вебхук для входящих (см. технический раздел в конце). Но даже с настроенным вебхуком сегодня ничего реально не отправляется: планировщик, который должен разбирать очередь, на продакшене ещё не настроен
- Бот не может пригласить себя в диалог — приглашает всегда тот, кто уже находится в этом диалоге или управляет им
- У одного бота не больше 2 действующих токенов одновременно, а на один аккаунт приходится максимум 5 ботов
С чего начать — четыре шага
- 1Создайте бота: задайте имя и имя пользователя (оно обязано заканчиваться на
bot, напримерorderbot). Если бот должен принадлежать заведению, а не лично вам, выберите заведение прямо при создании - 2Скопируйте токен: система покажет его ровно один раз после создания — сразу же сохраните его в надёжном месте. Закрыв экран, вернуть настоящее значение будет невозможно, останется только выпустить новый токен
- 3Пригласите бота в диалог: выберите личный диалог, комнату сообщества или комнату команды, где бот должен говорить. Приглашать может только тот, кто сам находится в этом диалоге или управляет им
- 4Отправьте первое сообщение: возьмите токен из шага 2 и вызовите Bot API — рабочий пример в следующем разделе
Шаги 1–3 делаются в приложении Korat
Меню находится по пути Настройки › Аккаунт › «Мои боты»: там можно создать бота, выпустить, обновить или отозвать токен и пригласить бота в диалог, комнату сообщества или комнату команды — без единой строки кода. Этот раздел появится в следующей версии приложения Korat: в той, которую скачивают сегодня, его ещё нет. Если в настройках «Моих ботов» не видно, дождитесь обновления. А пока шаги 1–3 можно выполнить напрямую через RPC Supabase (см. технический раздел в конце).
🔒 Токен — это пароль бота
Любой, у кого есть этот токен, немедленно может писать от имени бота. Не передавайте токен через переписку, картинки или незашифрованную почту — храните его только на своём сервере или в хранилище секретов. Ни один сотрудник Korat никогда не попросит у вас токен — ни в чате, ни по почте, ни по телефону. Если кто-то представляется командой Korat и просит токен, считайте это мошенничеством и немедленно сообщите через страницу Контакты. Если токен всё же утёк, отзовите его в приложении сразу (действие необратимо, придётся выпустить новый).
Пример — отправить первое сообщение
Точка входа одна: POST https://koratland.com/api/bot/send. Токен передаётся в заголовке, а в теле указывается, что и куда написать:
Через curl (для тех, кому привычен терминал)
curl -X POST "https://koratland.com/api/bot/send" \
-H "X-Bot-Token: kbot_12_xxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "kind": "chat", "chat_id": 9931, "text": "Поступил новый заказ" }'
Замените kbot_12_xxxxxxxxxxxxxxxxxxxx на настоящий токен бота, а 9931 — на номер диалога, куда бот приглашён (номер виден в ссылке на диалог в приложении или в списке диалогов на экране «Мои боты»).
Без программирования (Google Apps Script)
Если своего сервера нет, подойдёт бесплатный Google Apps Script с обычным аккаунтом Google: зайдите на script.google.com → новый проект → вставьте этот код → нажмите «Выполнить». При первом запуске он попросит разрешение на доступ в интернет (его можно дать: это ваш собственный скрипт):
function sendKoratMessage() {
var url = "https://koratland.com/api/bot/send";
var payload = {
kind: "chat",
chat_id: 9931, // подставьте номер своего диалога
text: "Поступил новый заказ" // подставьте нужный текст
};
var options = {
method: "post",
contentType: "application/json",
headers: { "X-Bot-Token": "kbot_12_xxxxxxxxxxxxxxxxxxxx" }, // подставьте токен своего бота
payload: JSON.stringify(payload)
};
var res = UrlFetchApp.fetch(url, options);
Logger.log(res.getContentText()); // результат смотрите в журнале выполнения
}
Одного нажатия «Выполнить» достаточно для проверки. Чтобы отправка шла сама — например, при каждой новой строке в Google Sheets с заказами, — воспользуйтесь триггерами Apps Script: вызывать функцию при изменении таблицы (onEdit) или раз в столько-то минут.
Точно так же это работает в любых инструментах автоматизации, где есть действие «HTTP request» или «Webhook» — n8n, Make, Zapier: укажите метод POST на тот же адрес, заголовок X-Bot-Token и такое же JSON-тело, как в примере выше.
Частые проблемы
Что видите (reason) | Что это значит | Как исправить |
|---|---|---|
not_invited | Бота не приглашали в этот диалог (или пригласили, а потом удалили) | Пригласите бота заново с экрана «Мои боты» или вызовом bot_invite |
invite_stale | Тот, кто пригласил бота, потерял права в этом диалоге (вышел из сообщества, понижен в роли, лишён управления комнатой команды, комната закрыта) ⇒ бот замолчал сам, хотя его никто не удалял | Пусть бота пригласит заново тот, у кого права в этом диалоге действительно есть: права всегда считаются по последнему пригласившему |
conversation_not_open | Это диалог заведения с клиентом, и клиент ещё ни разу не писал — заведение, включая его ботов, не может написать первым | Дождитесь сообщения клиента, и только потом пусть бот отвечает |
rate_limited | Бот шлёт сообщения чаще допустимого (в минуту или в сутки) | Подождите столько, сколько указано в retry_after_sec, и повторите. Если нужен более высокий лимит, напишите команде через Контакты |
blocked | Пользователь в этом диалоге заблокировал бота | Со стороны заведения это не исправить — при необходимости используйте другой диалог или другого бота |
| Бот писал, а потом внезапно замолчал | Чаще всего это тот самый invite_stale выше, а не поломка бота | Проверьте, остался ли пригласивший в диалоге и сохранил ли он права |
Полный перечень кодов ошибок — в справочной таблице технического раздела ниже.
Технический справочник (для разработчиков)
Дальше — для тех, кто будет обращаться к Bot API из своей программы: все параметры, полная таблица кодов ошибок и вебхук для входящих сообщений.
Шаги 1–3 напрямую через RPC (без приложения)
Если в вашей версии приложения экрана «Мои боты» ещё нет или всё нужно делать автоматически, вызывайте RPC Supabase напрямую через PostgREST, используя access token вошедшего пользователя (не токен бота). Три шага:
1. Создать бота — bot_create
curl -X POST "https://<SUPABASE_PROJECT>.supabase.co/rest/v1/rpc/bot_create" \
-H "apikey: <SUPABASE_ANON_KEY>" \
-H "Authorization: Bearer <USER_ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"p_name": "Бот уведомлений",
"p_username": "notifybot",
"p_business_id": null,
"p_about": "Сообщает о статусе заказов"
}'
p_username состоит из a–z 0–9 _, имеет длину 4–31 символ и всегда заканчивается на bot (регулярное выражение ^[a-z][a-z0-9_]{2,29}bot$). Передавайте p_business_id, если бот принадлежит заведению: вызывающий должен быть его сотрудником и обладать правом bot.manage. Успешный ответ содержит token — настоящее значение, которое выдаётся один-единственный раз за жизнь токена. База хранит только хеш sha256, поэтому потерянный токен не восстановить: остаётся выпустить новый (bot_rotate_token).
{ "ok": true, "bot_id": 12, "user_id": 4821, "username": "notifybot", "token": "kbot_12_…" }
2. Пригласить бота в диалог — bot_invite
Вызывающий должен действительно состоять в этом диалоге (или быть сотрудником заведения, если это диалог заведения) и иметь право управлять этим ботом (can_manage_bot). p_target — это chats.id.
curl -X POST "https://<SUPABASE_PROJECT>.supabase.co/rest/v1/rpc/bot_invite" \
-H "apikey: <SUPABASE_ANON_KEY>" \
-H "Authorization: Bearer <USER_ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{ "p_bot": 12, "p_kind": "chat", "p_target": 9931 }'
2а. Пригласить бота в комнату сообщества или команды — требования строже
Передайте p_kind: "community_room" или "business_room", а в p_target — community_rooms.id либо business_rooms.id: это уже работает. Но приглашающий обязан пройти проверку прав, соответствующую смыслу комнаты, а не просто состоять в ней:
- Комната сообщества — приглашающий должен реально иметь право писать в неё по её же правилам (
read_role/post_role). В обычной комнате для общения приглашать может участник, а в комнате объявлений или служебной, открытой только администраторам, — лишь владелец или администратор сообщества - Комната команды заведения — нужно пройти все проверки самой комнаты (право чтения, право публикации, правила получателей) плюс обязательно иметь право
teamroom.manage: привести бота во внутреннюю комнату компании — это «управление комнатой», а не «письмо в комнату»
В обоих случаях приглашающий должен быть ещё и управляющим этого бота (can_manage_bot): общая комната — не место, куда кто угодно втаскивает чужого бота.
🔴 invite_stale — самое важное, что нужно понять до запуска
Права приглашающего проверяются не только в момент приглашения: bot_send заново спрашивает правила комнаты при каждой отправке, от имени того же пригласившего. Как только приглашающий вышел из сообщества, понижен в роли, лишён teamroom.manage или комната закрыта, следующее сообщение бота немедленно получит reason: "invite_stale". Бот замолкает сам, и никто его при этом не удалял. Здесь нет фоновой задачи и нет триггера, который нужно не забыть вызвать: это чистый пересчёт прав.
Отправка сообщения — POST /api/bot/send
Токен передаётся одним из двух способов, на выбор:
# способ 1 — Authorization: Bearer
curl -X POST "https://koratland.com/api/bot/send" \
-H "Authorization: Bearer kbot_12_xxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-9931-shipped" \
-d '{ "kind": "chat", "chat_id": 9931, "text": "Посылка отправлена" }'
# способ 2 — X-Bot-Token
curl -X POST "https://koratland.com/api/bot/send" \
-H "X-Bot-Token: kbot_12_xxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "kind": "chat", "chat_id": 9931, "text": "Посылка отправлена" }'
Без токена вернётся 401:
{ "ok": false, "reason": "bad_token", "detail": "передавайте токен только в Authorization: Bearer … или X-Bot-Token" }
Поля тела запроса
| Поле | Тип | Обязательно | Значение |
|---|---|---|---|
kind | string | нет (по умолчанию "chat") | Определяет тип адресата. Допустимых значения три: "chat" · "community_room" · "business_room". Любое другое даёт reason: "bad_target" прямо из базы (молчаливого отката к "chat" нет — прежняя ошибка, из-за которой значение было зафиксировано, исправлена) |
chat_id | integer > 0 | да (или target_id) | Идентификатор адресата, куда бот приглашён: chats.id при kind: "chat", community_rooms.id при "community_room", business_rooms.id при "business_room". Имя поля в теле всегда остаётся chat_id или target_id, отдельных имён под каждый kind нет |
text | string | да | Текст сообщения. Пустым быть не может, длина ограничена значением из справочника text_max_message (лимит настраиваемый, а не жёсткое число — смотрите поле max в ошибке too_long) |
client_key | string | нет | Ключ защиты от дублей, замена заголовку Idempotency-Key: длина 8–128 символов, допустимы только A–Z a–z 0–9 _ . : - |
Личный диалог и диалог заведения — какое поле решает
kind определяет тип таблицы адресата (диалог, комната сообщества, комната команды). А вот личный диалог и диалог заведения по kind не различаются: и то и другое — строки в таблице chats, а отличает их наличие значения в chats.business_id (значение есть — это диалог заведения). Проверка прав на приглашение, проверка блокировок и правило «заведение не пишет клиенту первым» (conversation_not_open) читают именно эту колонку. Переданный chat_id должен указывать на правильный диалог: отдельного параметра «личный или деловой» в API нет.
Idempotency-Key и лимиты частоты
Передавайте либо стандартный заголовок Idempotency-Key, либо поле client_key в теле — что-то одно. Если пришло и то и другое, значения обязаны совпадать, иначе будет 422 idempotency_key_conflict. Повтор того же запроса с тем же ключом (та же комната и тот же ключ) возвращает прежний ответ 200 с полем "duplicate": true и второго сообщения не создаёт.
Лимитов два уровня: на IP (120 запросов в минуту, на самом Worker) и на бота (по умолчанию 20 в минуту и 1000 в сутки на одного бота, администратор меняет индивидуально). Оба отвечают кодом 429 и заголовком Retry-After в секундах (на IP — всегда 60 · на бота в минуту — 60 · на бота в сутки — 3600).
Приём входящих — вебхук
Когда кто-то пишет в диалог, куда приглашён бот (и отправитель — не сам бот: так исключается эхо собственных сообщений), Korat отправляет POST на заданный адрес. Настраивается это через RPC от имени вошедшего пользователя, который управляет этим ботом (can_manage_bot), — как и получение токена:
curl -X POST "https://<SUPABASE_PROJECT>.supabase.co/rest/v1/rpc/bot_set_webhook" \
-H "apikey: <SUPABASE_ANON_KEY>" \
-H "Authorization: Bearer <USER_ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{ "p_bot": 12, "p_url": "https://your-server.example.com/korat-webhook" }'
{ "ok": true, "secret": "a1b2c3…", "note": "этот secret показывается один раз — сохраните его для проверки подписи" }
Адрес обязан начинаться с https:// и не может указывать на внутренний, локальный или link-local хост (localhost, 127.0.0.1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, включая адреса метаданных всех облачных провайдеров). Такой адрес отклоняется уже при настройке и проверяется повторно, с настоящим разрешением DNS, перед каждой отправкой — на случай, если DNS позже перенаправят. secret виден только один раз, при настройке или обновлении: bot_rotate_webhook_secret(p_bot) обновляет его в любой момент, bot_disable_webhook(p_bot) и bot_enable_webhook(p_bot) выключают и включают вебхук, а bot_webhook_status(p_bot) показывает состояние (secret он не возвращает).
Сегодня наружу ничего не уходит
Очередь отправки (bot_webhook_deliveries) работает правильно и проверена в базе, секрет маршрута отправки (BOT_WEBHOOK_DISPATCH_SECRET) владельцем задан, но то, что разбирает очередь и отправляет (pg_cron, который должен вызывать Worker каждую минуту), на продакшене ещё не настроено. Вебхук настроить можно, сообщения встанут в очередь, но ваш адрес ничего не получит, пока не будет заведён планировщик pg_cron.
Как выглядит запрос, который придёт вам
POST /korat-webhook HTTP/1.1
Content-Type: application/json
X-Korat-Signature: sha256=<hex>
X-Korat-Timestamp: 1750000000000
{
"event": "message.created",
"chat_id": 9931,
"message_id": 88213,
"text": "А красный есть в наличии?",
"time": "14:02",
"sender": { "id": 4821, "username": "ivan_p", "name": "Иван П." }
}
И это всё — ни телефона, ни почты, ни чьих-либо токенов в теле нет. Бот видит только диалог, текст и минимум сведений об отправителе, необходимый для ответа.
Проверьте подпись, прежде чем доверять запросу
X-Korat-Signature — это HMAC-SHA256(secret, "<timestamp>." + rawBody) в шестнадцатеричном виде. Пример на Node.js:
const crypto = require("crypto");
function verify(rawBody, signatureHeader, timestampHeader, secret) {
const age = Date.now() - Number(timestampHeader);
if (!(age >= 0 && age < 5 * 60_000)) return false; // защита от повторов: не старше 5 минут
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(`${timestampHeader}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
Ответ 2xx в течение 8 секунд считается успешной доставкой. Любой другой ответ (включая перенаправления 3xx, за которыми Korat не следует) или отсутствие ответа считается неудачей, и доставка ставится в очередь на повтор.
Повторы, когда адресат недоступен
При неудаче идёт экспоненциальная задержка (1, 2, 4, 8, 16, 32, 64, 128 минут), максимум 8 попыток на сообщение, после чего это сообщение больше не отправляется. Если адресат подряд отказал 15 раз (по разным сообщениям), вебхук выключается автоматически, а владелец бота получает уведомление в приложении — бесконечно долбиться в мёртвый адрес система не будет. После устранения проблемы включите его снова вызовом bot_enable_webhook.
Полная таблица кодов ошибок
В любом неуспешном ответе всегда есть {"ok": false, "reason": "…"}: решение нужно принимать по reason, а не по одному лишь HTTP-статусу.
| HTTP | reason | Источник | Значение и что делать |
|---|---|---|---|
| 401 | bad_token | Worker / БД | Токен не передан, неверен, отозван либо бот отключён — ответ намеренно один и тот же, чтобы нельзя было подобрать перебором |
| 403 | not_invited | БД | Бота не приглашали в этот диалог или удалили из него — сначала вызовите bot_invite |
| 403 | blocked | БД | Пользователь в диалоге заблокировал бота (только в диалогах, не относящихся к заведению) |
| 403 | account_suspended | БД | Одна из сторон диалога заблокирована — это вопрос прав, а не ошибки запроса |
| 404 | no_such_chat | БД | Такого chat_id не существует |
| 404 | no_such_endpoint | Worker | Неверный путь: существует только POST /api/bot/send |
| 405 | method_not_allowed | Worker | Использован метод, отличный от POST |
| 409 | conversation_not_open | БД | Это диалог заведения, и клиент ещё не писал — заведение, включая бота, не может начать первым |
| 409 | chat_closed | БД | Диалог в знакомствах больше не принимает сообщения |
| 422 | empty_text | БД | text пуст после удаления пробелов |
| 422 | too_long | БД | Текст длиннее лимита — смотрите приложенное значение max |
| 422 | bad_target | БД | kind не равен ни одному из "chat"/"community_room"/"business_room" |
| 404 | no_such_room | БД | Комнаты сообщества или команды с таким chat_id не существует (она удалена) |
| 409 | room_archived | БД | Комната закрыта — писать не могут ни люди, ни боты |
| 403 | invite_stale | БД | Права пригласившего пересчитаны и больше не проходят: он вышел из сообщества, понижен в роли, лишён teamroom.manage или комната закрыта. Уточнение в detail (not_a_member/room_role/not_teamroom_manager). Лечится тем, что бота заново приглашает тот, у кого права есть (bot_invite), а не ожиданием |
| 422 | bad_json | Worker | Тело запроса не является корректным JSON |
| 422 | bad_chat_id | Worker | chat_id или target_id должен быть целым положительным числом |
| 422 | bad_text | Worker | text должен быть строкой |
| 422 | idempotency_key_conflict | Worker | Idempotency-Key и client_key пришли вместе, но не совпадают — достаточно передать что-то одно |
| 422 | bad_client_key | Worker | Ключ защиты от дублей не соответствует формату (8–128 символов, A–Z a–z 0–9 _ . : -) |
| 429 | rate_limited | Worker / БД | Лимит превышен — смотрите заголовок Retry-After и поля window/retry_after_sec |
| 502 | database_error | Worker | База ответила, но не кодом 2xx — в pg_code и detail приходит настоящее сообщение Postgres |
| 503 | service_key_not_configured | Worker | На сервере не задан service key — это проблема на стороне Korat, а не вызывающего |
| 504 | database_unreachable | Worker | Обращение к базе не удалось или истекло время — смотрите safe_to_retry: повторять безопасно только если вы передавали client_key либо Idempotency-Key |
Ответ при успехе
{ "ok": true, "duplicate": false, "message_id": 88213 }
duplicate: true означает, что этот запрос уже отправлялся раньше (защита по client_key или Idempotency-Key): нового сообщения не создано, но это по-прежнему успех (200), потому что нужный вызывающему результат уже достигнут.