Эндпоинт POST /{channel}/messages отправляет одному человеку одно сообщение от имени вашего бота. Это самый простой способ достучаться до клиента из своего кода: «заказ собран», «код подтверждения», «оплата прошла».
Полный адрес:
POST https://etochat.bot/api/v2/{channel}/messages
Сначала — главное ограничение
Написать человеку можно только если он раньше сам обратился к боту. До первого обращения адреса доставки просто не существует: Telegram позволяет писать лишь тем, кто хотя бы раз запускал бота, ВКонтакте требует от человека разрешения на сообщения от сообщества, у чата на сайте до первого визита нет самого контакта.
Это ограничение мессенджеров, а не наше, и обойти его нельзя. Поэтому сценарий «выгрузим базу клиентов из CRM и разошлём всем напоминания» через этот эндпоинт работать не будет — доставятся только сообщения тем, кто уже писал боту.
Практический вывод: сначала приводите людей в бота (кнопка на сайте, ссылка в письме, виджет чата), а уже потом стройте на API рассылку событий.
Ещё одно ограничение — канал. Работают чат на сайте, Telegram, ВКонтакте и МАКС. Instagram не поддерживается: запрос ключом от Instagram-бота вернёт 400 unsupported_channel.
Заголовки
Authorization: Bearer etchtbt_bot_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
Ключ выпускается в кабинете: выберите бота → «Для разработчиков» → «Ключи API» → «Выпустить ключ». Ключ привязан к одному боту и не открывает другие боты того же владельца.
Поля тела запроса
| Поле | Тип | Обязательно | Что это |
|---|---|---|---|
chat_id | строка | да, если нет external_id | Идентификатор контакта в нашей системе |
external_id | строка | да, если нет chat_id | Ваш идентификатор пользователя, ранее привязанный через POST /{channel}/contacts/link |
text | строка | да | Текст сообщения, до 4000 символов, допускает HTML-разметку |
buttons | массив | нет | До 8 кнопок-ссылок, каждая — объект {"text": "…", "url": "https://…"} |
Получателя нужно указать ровно одним способом — либо chat_id, либо external_id. Если не передать ни того, ни другого, придёт 400 recipient_required.
Про chat_id
chat_id в ответах API всегда приходит строкой, и передавать его тоже лучше строкой. Это не формальность: у чата на сайте идентификатор длиннее 16 цифр, и если положить его в обычное число JavaScript, точность потеряется — вы отправите сообщение «в никуда» или получите 404 contact_not_found.
Где взять chat_id:
- запросом
GET /{channel}/chats(есть поиск и постраничный вывод); - на странице «Для разработчиков» можно указать до 3 адресов, куда мы зеркалим входящие сообщения — туда придёт и
chat_idв момент, когда человек написал боту.
Про text
Лимит — 4000 символов на значение поля text. Поле поддерживает HTML-разметку, но разные мессенджеры понимают разный набор тегов, поэтому держитесь минимума — простое выделение и ссылки — а если сомневаетесь, отправляйте обычный текст. Любые пользовательские данные, которые вы подставляете в текст (имя, название товара, комментарий из формы), экранируйте: символы <, > и & в сыром виде могут сломать разметку и привести к 502 send_failed.
Про buttons
Кнопки — только ссылки. У каждой два поля: text (подпись) и url (обязательно http:// или https://). Кнопок можно приложить до 8. Кнопок-действий, которые продолжают сценарий, здесь нет — если нужно ветвление по нажатию, это уже задача для POST /{channel}/trigger (см. ниже).
Примеры
Простое сообщение, curl
curl -X POST https://etochat.bot/api/v2/{channel}/messages \
-H "Authorization: Bearer etchtbt_bot_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{
"chat_id": "9995735656",
"text": "Заказ №1043 собран и передан в доставку."
}'
Сообщение с кнопкой, curl
curl -X POST https://etochat.bot/api/v2/{channel}/messages \
-H "Authorization: Bearer etchtbt_bot_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{
"chat_id": "9995735656",
"text": "Заказ <b>№1043</b> в пути. Отследить можно по ссылке ниже.",
"buttons": [
{"text": "Отследить заказ", "url": "https://example.com/orders/1043"},
{"text": "Написать в поддержку", "url": "https://example.com/support"}
]
}'
Отправка по своему идентификатору, curl
Если вы один раз привязали своего пользователя к контакту через POST /{channel}/contacts/link, дальше можно вообще не хранить chat_id у себя:
curl -X POST https://etochat.bot/api/v2/{channel}/messages \
-H "Authorization: Bearer etchtbt_bot_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{
"external_id": "user-42",
"text": "Подписка продлена до 30 августа."
}'
PHP
<?php
function etochatSend(string $apiKey, array $payload): array
{
$ch = curl_init('https://etochat.bot/api/v2/{channel}/messages');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
]);
$body = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return ['status' => $status, 'data' => json_decode($body, true)];
}
$res = etochatSend('etchtbt_bot_ВАШ_КЛЮЧ', [
'chat_id' => '9995735656',
'text' => 'Оплата на 2 400 ₽ получена. Спасибо!',
'buttons' => [
['text' => 'Смотреть чек', 'url' => 'https://example.com/receipt/8891'],
],
]);
if ($res['status'] === 200) {
echo 'Отправлено, id сообщения: ' . $res['data']['message_id'];
} else {
echo 'Ошибка ' . $res['status'] . ': ' . $res['data']['error']['code'];
}
Python
import requests
API = "https://etochat.bot/api/v2/{channel}"
KEY = "etchtbt_bot_ВАШ_КЛЮЧ"
def send_message(payload: dict) -> dict:
r = requests.post(
f"{API}/messages",
headers={"Authorization": f"Bearer {KEY}"},
json=payload,
timeout=20,
)
data = r.json()
if r.status_code != 200:
code = data.get("error", {}).get("code", "unknown")
raise RuntimeError(f"{r.status_code} {code}: {data.get('error', {}).get('message', '')}")
return data
# по внешнему идентификатору
result = send_message({
"external_id": "user-42",
"text": "Осталось 3 дня подписки. Продлить можно в один клик.",
"buttons": [{"text": "Продлить", "url": "https://example.com/billing"}],
})
print(result["message_id"])
Ответ
Успешный ответ — 200 и такой JSON:
{
"sent": true,
"message_id": 16641,
"chat_id": "9995735656"
}
sent— сообщение принято мессенджером;message_id— идентификатор сообщения в нашей базе. Пригодится, чтобы связать отправку со своей записью в логах;chat_id— контакт, которому ушло сообщение. Особенно полезен, когда вы отправляли поexternal_id: так вы узнаетеchat_id, не делая отдельный запрос.
У эндпоинта нет ключа идемпотентности: два одинаковых запроса дадут два сообщения. Если ваш код может повторить запрос (ретрай очереди, повторный вебхук от платёжной системы), отсекайте дубли у себя — например, отмечайте у заказа факт отправки до того, как выполнить повтор.
Ошибки
Ошибка всегда приходит одинаковым JSON:
{
"error": {
"code": "recipient_blocked_bot",
"message": "Пользователь остановил бота"
}
}
Ориентируйтесь на code, а не на текст message — текст может меняться.
| HTTP | code | Что произошло |
|---|---|---|
| 401 | unauthorized | Не передан заголовок Authorization |
| 401 | invalid_key | Ключ неверен, отозван или бот отключён |
| 400 | unsupported_channel | Канал бота не поддерживается (Instagram) |
| 400 | recipient_required | Не указан ни chat_id, ни external_id |
| 400 | chat_id_invalid | chat_id не похож на идентификатор |
| 400 | external_id_required / external_id_too_long | Внешний id пустой или длиннее 190 символов |
| 400 | text_required / text_too_long | Нет текста или он длиннее 4000 символов |
| 402 | subscription_inactive | Подписка неактивна |
| 404 | contact_not_found | У этого бота нет такого контакта |
| 404 | external_id_not_linked | Внешний id никому не привязан |
| 409 | recipient_blocked_bot | Человек остановил бота |
| 502 | send_failed | Мессенджер не принял сообщение |
Что делать в каждом случае
402 subscription_inactive. Отправка выключена, потому что подписка на бота неактивна. Никакие повторы не помогут — нужно продлить подписку в кабинете. Проверить состояние заранее можно запросом GET /{channel}/me: поле can_send покажет false. Если у вас очередь исходящих, при 402 имеет смысл поставить её на паузу и уведомить администратора, а не сжигать попытки.
404 contact_not_found. Такого контакта у бота нет. Самая частая причина — человек ещё ни разу не писал боту, вторая по частоте — испорченный chat_id: его положили в число и потеряли последние цифры. Проверьте, что передаёте строку, и сверьтесь с GET /{channel}/chats. Повторять запрос бессмысленно, пока человек не обратится к боту сам.
404 external_id_not_linked. Вы отправляете по своему идентификатору, но он ни к кому не привязан. Сначала вызовите POST /{channel}/contacts/link с парой chat_id + external_id, потом повторите отправку. Удобный порядок действий: привязывать сразу в момент, когда человек впервые написал боту, а не в момент отправки.
409 recipient_blocked_bot. Человек остановил бота (в Telegram — «Stop», во ВКонтакте — запретил сообщения). Доставить сообщение невозможно до тех пор, пока он не напишет боту снова. Правильная реакция — не ретраить, а пометить контакт у себя как недоступного и исключить из будущих отправок. В GET /{channel}/chats такие контакты видны по полю blocked_bot.
502 send_failed. Мессенджер не принял сообщение: временный сбой на его стороне, слишком длинный или битый HTML, недоступный url в кнопке. Разумная стратегия — один-два повтора с паузой (например, через 5 и 30 секунд). Если повторяется стабильно, упростите сообщение до чистого текста без разметки и кнопок: так вы за один шаг поймёте, дело в разметке или в канале.
400 text_too_long. Лимит 4000 символов. Не обрезайте текст вслепую посередине HTML-тега — разобьёте разметку. Либо режьте по абзацам и отправляйте двумя сообщениями, либо (лучше) оставьте в сообщении суть и кнопку-ссылку на полную версию.
Когда лучше не POST /{channel}/messages, а POST /{channel}/trigger
POST /{channel}/messages — «немой» эндпоинт: он доставляет ровно тот текст, который вы прислали, и на этом всё. Это его достоинство и его потолок.
Берите вместо него POST /{channel}/trigger, если верно хотя бы одно:
- Текст должен жить в конструкторе. Формулировки, эмодзи, порядок абзацев — то, что маркетолог или владелец бота хочет править сам, без релиза вашего кода. В
triggerвы передаёте только данные ({"vars": {"plan": "Премиум", "days_left": "3"}}), а текст с подстановками{{var.plan}}и{{var.days_left}}редактируется в кабинете. - Нужно несколько шагов. Приветствие, пауза, второе сообщение, картинка, файл — цепочку удобнее собрать блоками в конструкторе, чем выстраивать таймерами у себя.
- Нужны условия и ветвление. «Если тариф премиум — одно, иначе другое», «если человек не ответил за сутки — напомнить». В
POST /{channel}/messagesлогики нет вообще. - Нужны кнопки, которые ведут дальше по сценарию. Здесь доступны только кнопки-ссылки. Кнопки, по нажатию которых бот задаёт следующий вопрос или сохраняет ответ, существуют только внутри сценария.
- Нужен диалог, а не уведомление. Собрать ответ пользователя, записать его в переменную, позвать оператора — это всё сценарий.
Оставляйте POST /{channel}/messages, когда сообщение одноразовое, полностью формируется вашим кодом и никакого продолжения не подразумевает: код подтверждения, статус заказа, системное предупреждение.
Технически разница ещё и в подготовке. Для trigger нужно один раз создать в конструкторе сценарий с триггером «Из другой автоматизации» и включить его — иначе придёт 404 no_flow. Для messages готовить ничего не надо: выпустили ключ и отправляете.
Короткий чек-лист перед первым запросом
- Ключ выпущен для нужного бота и канал бота не Instagram.
GET /{channel}/meотвечает200иcan_send: true.- Получатель уже писал боту, его
chat_idвзят изGET /{channel}/chatsи передаётся строкой. - Текст короче 4000 символов, пользовательские данные в нём экранированы.
- В коде разобраны 402, 404, 409 и 502 — и на 409 вы помечаете контакт недоступным, а не повторяете отправку.