Эта статья — справочник, к которому возвращаются, когда что-то пошло не так. Здесь все коды ошибок API, объяснение ограничений (почему нельзя написать человеку первым) и ответы на частые вопросы.
Базовый адрес API: https://etochat.bot/api/v2/{channel}. Авторизация — заголовок Authorization: Bearer <ключ>.
Как выглядит ошибка
Любая ошибка приходит одинаково: JSON с объектом error и осмысленный HTTP-статус.
{
"error": {
"code": "recipient_blocked_bot",
"message": "Человек остановил бота"
}
}
В своём коде ориентируйтесь на error.code, а не на текст message: код стабилен, текст может быть переформулирован. HTTP-статус удобен для грубой логики (повторять запрос или нет), code — для точной.
Таблица кодов ошибок
| HTTP | code | Что значит | Что делать |
|---|---|---|---|
| 401 | unauthorized | Заголовок Authorization не передан вообще | Добавьте заголовок Authorization: Bearer etchtbt_bot_…. Частая причина — прокси или библиотека, вырезающая заголовки |
| 401 | invalid_key | Ключ неверен, отозван или бот отключён | Проверьте ключ через GET /{channel}/me. Если ключ был отозван — выпустите новый: кабинет → бот → «Для разработчиков» → «Ключи API» |
| 400 | unsupported_channel | Канал бота не поддерживается этим API | Instagram-боты через клиентское API не работают. См. раздел «Частые вопросы» |
| 400 | recipient_required | Не указан ни chat_id, ни external_id | Передайте один из них в теле запроса |
| 400 | chat_id_invalid | chat_id пустой или в неверном формате | Берите chat_id из GET /{channel}/chats и передавайте строкой |
| 400 | text_required | Поле text отсутствует или пустое | Текст сообщения обязателен |
| 400 | text_too_long | Текст длиннее 4000 символов | Сократите или разбейте на несколько сообщений |
| 400 | external_id_required | В POST /{channel}/contacts/link не передан external_id | Добавьте поле external_id |
| 400 | external_id_too_long | external_id длиннее 190 символов | Используйте короткий идентификатор — id пользователя, а не длинную строку с метаданными |
| 402 | subscription_inactive | Подписка на боте неактивна, отправка запрещена | Продлите подписку в кабинете. До этого запросы на отправку будут отклоняться |
| 404 | contact_not_found | У этого бота нет контакта с таким chat_id | Проверьте, что chat_id от нужного бота: ключ привязан к одному боту и чужие контакты не видит |
| 404 | external_id_not_linked | Внешний идентификатор никому не привязан | Сначала вызовите POST /{channel}/contacts/link, потом отправляйте по external_id |
| 404 | no_flow | Нет включённого сценария с триггером «Из другой автоматизации» | Создайте такой сценарий в конструкторе и включите его |
| 409 | recipient_blocked_bot | Человек остановил бота или запретил сообщения | Не повторяйте запрос. Пометьте контакт у себя как недоступный |
| 502 | send_failed | Мессенджер не принял сообщение | Временная неполадка на стороне мессенджера. Повторите запрос с паузой |
Ограничения
Написать можно только тем, кто уже писал боту
Это главное ограничение, и оно не наше — так устроены сами мессенджеры. До первого обращения человека к боту адреса доставки просто не существует: нет chat_id, некуда отправлять.
- Telegram. Бот может писать только тем, кто хотя бы раз запускал его — нажал «Старт» или отправил сообщение. Это правило платформы, обойти его нельзя ничем.
- ВКонтакте. Сообщество может писать пользователю, только если тот разрешил сообщения от сообщества. Обычно разрешение появляется, когда человек сам начинает диалог.
- Чат на сайте.
chat_idпоявляется в момент, когда посетитель открыл виджет и написал. Пока диалога нет, писать некому. - МАКС. Так же: диалог должен быть начат пользователем.
Практический вывод: план «выгрузим базу клиентов из CRM и разошлём всем напоминания в Telegram» не сработает. Работает другой план — сначала привести человека в бота (кнопкой на сайте, ссылкой в письме, QR-кодом), а потом уже общаться с ним через API.
Подписка должна быть активной
Если подписка на боте неактивна, отправка отклоняется с 402 subscription_inactive. Проверить состояние можно заранее:
curl -s https://etochat.bot/api/v2/{channel}/me \
-H "Authorization: Bearer etchtbt_bot_ВАШ_КЛЮЧ"
{
"bot": {"id": 83, "name": "Мой бот", "channel": "webchat", "username": ""},
"can_send": true
}
Поле can_send: false — сигнал, что отправка сейчас невозможна. Удобно вызывать GET /{channel}/me в мониторинге раз в час: увидите проблему раньше, чем не уйдёт важное сообщение.
Человек мог остановить бота
Любой пользователь в любой момент может нажать «Стоп» или заблокировать бота. Тогда отправка вернёт 409 recipient_blocked_bot. Мы такие контакты отмечаем — в списке контактов у них "blocked_bot": true.
curl -s "https://etochat.bot/api/v2/{channel}/chats?limit=100" \
-H "Authorization: Bearer etchtbt_bot_ВАШ_КЛЮЧ"
Перед массовой отправкой имеет смысл пропускать контакты с blocked_bot: true — сэкономите время и не будете разбирать лишние ошибки.
Ограничения на значения
| Что | Ограничение |
|---|---|
text в POST /{channel}/messages | до 4000 символов, поддерживает HTML-разметку |
buttons | до 8 штук, только кнопки-ссылки: text + url, url обязан быть http или https |
vars в POST /{channel}/trigger | до 20 переменных, значение до 500 символов |
| Имена переменных | латиница, цифры и подчёркивание |
external_id | до 190 символов, регистр сохраняется |
limit в GET /{channel}/chats | по умолчанию 50, максимум 100 |
| Ключи API | до 5 живых ключей на одного бота |
Ключ привязан к одному боту
Ключ открывает ровно один бот — тот, для которого он выпущен. Он не даёт доступа к другим ботам того же владельца. Если у вас три бота и вы интегрируете все три, выпустите три ключа и храните их отдельно.
Ключ показывается один раз при выпуске. В базе лежит только его хэш (sha256), восстановить исходное значение невозможно — потеряли, значит отзываете и выпускаете новый.
Частые вопросы
Можно ли разослать сообщение по всей клиентской базе?
Нет — только тем из вашей базы, кто уже писал боту. Список таких людей вы получаете через GET /{channel}/chats. Всем остальным доставить сообщение технически невозможно: у них нет диалога с ботом, а значит нет и адреса доставки.
Если задача — именно охватить всю базу, сначала нужно завести людей в бота: ссылка на бота в письме, кнопка на сайте, виджет чата на страницах, QR-код на упаковке. По мере того как люди пишут боту, они появляются в GET /{channel}/chats и становятся доступны для API.
Можно ли написать человеку первым?
Да, но только тому, кто раньше уже обращался к боту. «Первым» в смысле «мы начинаем новый разговор с существующим контактом» — можно и нужно: напомнить о брошенной корзине, сообщить о смене статуса заказа, предупредить об окончании подписки.
«Первым» в смысле «человек никогда не взаимодействовал с ботом» — нельзя ни в одном канале.
Поддерживается ли Instagram?
Нет. Клиентское API работает с четырьмя каналами: чат на сайте, Telegram, ВКонтакте, МАКС. У Instagram жёсткие правила Meta на исходящие сообщения, поэтому произвольная отправка через API там невозможна.
Запрос ключом от Instagram-бота вернёт:
{
"error": {
"code": "unsupported_channel",
"message": "Канал бота не поддерживается"
}
}
Для Instagram есть отдельное партнёрское API — напишите нам, если оно вам нужно.
Что будет при повторной привязке external_id?
Привязка переедет на новый контакт. Повторный POST /{channel}/contacts/link с тем же external_id, но другим chat_id, отвяжет идентификатор от старого контакта и привяжет к новому.
curl -s -X POST https://etochat.bot/api/v2/{channel}/contacts/link \
-H "Authorization: Bearer etchtbt_bot_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"chat_id":"9995735656","external_id":"user-42"}'
{"linked": true, "external_id": "user-42", "chat_id": "9995735656"}
Это удобно, когда человек пришёл к боту с нового устройства или из другого канала: вы просто перепривязываете user-42 на актуальный chat_id, и вся ваша логика продолжает работать по вашему же идентификатору.
Обратите внимание: регистр сохраняется, User-42 и user-42 — разные идентификаторы.
Почему chat_id — строка, а не число?
Потому что у чата на сайте он длиннее 16 цифр, и JavaScript при разборе такого JSON молча теряет точность — получится другой идентификатор, и сообщение уйдёт «в никуда». Поэтому мы всегда отдаём chat_id строкой и просим хранить его строкой: в базе — VARCHAR, в коде — строковый тип, никаких parseInt и приведения к int.
Как понять, что сообщение доставлено?
Успешный ответ выглядит так:
{"sent": true, "message_id": 16641, "chat_id": "9995735656"}
Он означает, что мессенджер принял сообщение в доставку и оно записано в диалог — вы увидите его в кабинете. Это не то же самое, что «человек прочитал»: признака прочтения API не отдаёт.
Если мессенджер сообщение не принял, вы получите ошибку — 502 send_failed при технической неполадке или 409 recipient_blocked_bot, если человек остановил бота. Состояния «вроде отправилось, а вроде нет» не бывает: либо sent: true, либо ошибка с кодом.
Поле message_id стоит сохранять у себя — по нему удобно сопоставлять ваши логи с диалогом в кабинете при разборе жалоб.
Что делать при 502?
502 send_failed — это «мессенджер сейчас не принял, попробуйте ещё раз». Причины обычно временные: перегрузка на стороне мессенджера, сетевой сбой. Правильная реакция — повторить запрос через несколько секунд, 2–3 раза, с увеличивающейся паузой.
Если 502 повторяется на всех сообщениях подряд дольше нескольких минут — скорее всего, проблема на стороне мессенджера. Поставьте отправку в очередь и разберите её позже, а не выжигайте попытки вхолостую.
Что означает 404 no_flow?
Вы вызвали POST /{channel}/trigger, но у бота нет включённого сценария с триггером «Из другой автоматизации». Такой сценарий надо создать в конструкторе автоматизаций и обязательно включить — выключенный сценарий тоже даёт no_flow.
Откуда взять chat_id?
Два способа. Первый — GET /{channel}/chats: там все, кто писал боту. Второй — на странице «Для разработчиков» можно указать до 3 адресов, куда мы будем зеркалить входящие сообщения; в такое уведомление приходит и chat_id ровно в тот момент, когда человек написал боту. Второй способ удобнее, если вы хотите привязывать external_id сразу, без периодического опроса списка контактов.
Рекомендации по надёжности
Повторять при 502, не повторять при 4xx
Простое правило:
- 502 — временная неполадка, повторяйте с растущей паузой: 1 с, 3 с, 9 с. Дальше — в очередь на потом.
- 4xx — повторять бессмысленно, ответ не изменится. Ошибка в вашем запросе или в состоянии контакта, её нужно чинить, а не ретраить.
- 402 subscription_inactive — не повторяйте в цикле. Остановите отправку, оповестите ответственного, продлите подписку.
- 409 recipient_blocked_bot — не повторяйте никогда. Пометьте контакт у себя и исключите из будущих отправок.
- 404 external_id_not_linked — единственное разумное исключение из правила «4xx не повторяем»: сделайте
POST /{channel}/contacts/linkи повторите отправку один раз.
Пример на bash — три попытки, повтор только на 502:
#!/bin/bash
KEY="etchtbt_bot_ВАШ_КЛЮЧ"
BODY='{"chat_id":"9995735656","text":"Заказ №1024 собран и ждёт вас"}'
for delay in 1 3 9; do
RESP=$(curl -s -w '\n%{http_code}' -X POST \
https://etochat.bot/api/v2/{channel}/messages \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d "$BODY")
CODE=$(printf '%s' "$RESP" | tail -n1)
JSON=$(printf '%s' "$RESP" | sed '$d')
echo "etochat $CODE $JSON"
if [ "$CODE" = "502" ]; then
sleep "$delay"
continue
fi
break
done
Та же логика на JavaScript:
async function sendMessage(chatId, text) {
const delays = [1000, 3000, 9000];
for (let i = 0; i < delays.length; i++) {
const res = await fetch('https://etochat.bot/api/v2/{channel}/messages', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + process.env.ETOCHAT_KEY,
'Content-Type': 'application/json'
},
// chat_id всегда строка: без Number() и parseInt()
body: JSON.stringify({ chat_id: String(chatId), text: text })
});
const data = await res.json();
console.log('etochat', res.status, JSON.stringify(data));
if (res.status === 502 && i < delays.length - 1) {
await new Promise(r => setTimeout(r, delays[i]));
continue;
}
if (!res.ok) throw new Error(data.error.code);
return data;
}
}
Логируйте ответы у себя
Сохраняйте по каждому вызову: время, эндпоинт, chat_id (или external_id), HTTP-статус, error.code при ошибке и message_id при успехе. Этого достаточно, чтобы через неделю ответить на вопрос «а почему клиенту не пришло напоминание» за минуту, а не за час.
Отдельно считайте долю ошибок по кодам. Резкий рост recipient_blocked_bot — сигнал, что вы пишете слишком часто или не по делу. Рост contact_not_found — скорее всего, у вас в базе протухли chat_id или запросы уходят с ключом не от того бота.
Ключи API в логи не пишите. Храните их в переменных окружения или в секретах, а не в коде и не в репозитории. Если ключ всё же засветился — отзовите его в кабинете и выпустите новый, это занимает несколько секунд.
Не держите отправку в основном потоке
Если сообщение уходит в ответ на действие пользователя на вашем сайте (оформил заказ — получил уведомление в боте), выносите вызов API в фоновую очередь. Тогда медленный ответ мессенджера или временный 502 не затормозят оформление заказа и не покажут вашему клиенту ошибку.
Если ошибка не описана в таблице или поведение API кажется неправильным — напишите нам в поддержку и приложите код ошибки, HTTP-статус, время запроса и id бота из GET /{channel}/me. С этими данными мы найдём ваш запрос в логах и ответим по существу.