Чтобы что-то отправить человеку через API — сообщение или запуск сценария — нужно объяснить нам, кому именно. Для этого есть два способа адресации: наш chat_id и ваш собственный external_id, привязанный к контакту. Разберём оба.
chat_id — наш адрес человека
chat_id — это идентификатор конкретного человека в конкретном боте. Он появляется в тот момент, когда человек впервые написал вашему боту: нажал «Старт» в Telegram, отправил сообщение в чате на сайте, написал в сообщения сообщества ВКонтакте.
Один и тот же человек в двух ваших ботах — это два разных контакта с разными chat_id. Ключ API привязан к одному боту, поэтому и chat_id вы всегда используете в пределах этого бота: чужой идентификатор просто не найдётся.
Главное ограничение, и оно не наше. Написать человеку можно только если он раньше сам обратился к боту. До первого обращения адреса доставки не существует ни в одном канале — так устроены сами мессенджеры. Telegram разрешает писать только тем, кто хотя бы раз запускал бота; ВКонтакте требует от человека разрешения на сообщения от сообщества. Поэтому сценарий «выгрузим базу клиентов из CRM и разошлём всем напоминания» не заработает: рассылать можно только тем, кто уже писал боту.
Если попробовать обратиться к человеку, которого бот не знает, вы получите честный отказ:
{
"error": {
"code": "contact_not_found",
"message": "Контакт не найден. Человек должен сначала написать боту — до этого адреса доставки не существует."
}
}
Два способа узнать chat_id
Способ 1: запросить список контактов
GET /{channel}/chats отдаёт всех, кто писал боту. Подходит, чтобы разово синхронизировать базу или найти человека по имени. Подробности — ниже в этой статье.
Способ 2: зеркалирование входящих
В кабинете на странице «Для разработчиков» можно указать до трёх адресов, куда мы будем зеркалить входящие сообщения. Как только человек написал боту, на ваш адрес прилетает событие — и в нём есть chat_id.
Это удобнее для живой интеграции: вы узнаёте адрес в момент первого контакта и сразу сохраняете его у себя рядом с профилем пользователя, вместо того чтобы периодически перечитывать весь список контактов.
Важно: chat_id — это строка
В ответах API chat_id всегда приходит строкой, и в теле запросов его тоже лучше отправлять строкой. Это не каприз формата.
У чата на сайте идентификаторы длиннее 16 цифр, а числа в JavaScript — это 64-битные float. Всё, что длиннее ~16 значащих цифр, молча округляется. Проверьте сами:
// пришло от API строкой
const chatId = "1753086420123456789";
Number(chatId); // 1753086420123456800 ← последние цифры потеряны
String(Number(chatId)); // "1753086420123456800"
Самое неприятное — что ошибка не выбрасывается. Вы получаете «почти правильный» идентификатор, отправляете его нам и ловите contact_not_found на человеке, который совершенно точно писал боту.
Та же ловушка срабатывает при парсинге JSON, если идентификатор где-то по пути превратился в число:
JSON.parse('{"chat_id":1753086420123456789}');
// { chat_id: 1753086420123456800 } ← сломалось ещё до вашего кода
Практические выводы:
- Храните
chat_idв текстовом поле базы (VARCHAR), а не вBIGINT, если ваш язык не гарантирует 64-битную целочисленную арифметику. - Не прогоняйте его через
parseInt,Number,+chatIdи подобное. - В электронных таблицах при экспорте тоже задавайте текстовый формат — иначе они округлят молча, как и JS.
GET /{channel}/chats — список людей
Возвращает контакты бота, отсортированные по свежести: последние писавшие — первыми.
Параметры запроса
| Параметр | По умолчанию | Описание |
|---|---|---|
limit | 50 | Сколько контактов вернуть. Максимум — 100. |
offset | 0 | Сколько пропустить с начала. Для постраничного обхода. |
query | — | Поиск по имени, фамилии и username. Ищет подстроку. |
Запрос
curl -s "https://etochat.bot/api/v2/{channel}/chats?limit=2" \
-H "Authorization: Bearer etchtbt_bot_ВАШ_КЛЮЧ"
Ответ
{
"total": 123,
"limit": 2,
"offset": 0,
"contacts": [
{
"chat_id": "9995735656",
"name": "Иван",
"username": "ivan",
"status": "open",
"unread": 0,
"blocked_bot": false,
"external_ids": ["user-42"],
"created_at": "2026-07-18 11:04:12",
"updated_at": "2026-07-21 09:31:50"
},
{
"chat_id": "1753086420123456789",
"name": "Гость с сайта",
"username": "",
"status": "open",
"unread": 2,
"blocked_bot": false,
"external_ids": [],
"created_at": "2026-07-21 08:12:03",
"updated_at": "2026-07-21 08:20:44"
}
]
}
Поля контакта
| Поле | Что это |
|---|---|
chat_id | Наш адрес человека. Строка. |
name | Имя и фамилия, как их отдал мессенджер. |
username | Username, если канал его сообщает. Часто пустой. |
status | Состояние диалога в инбоксе, например open. |
unread | Сколько непрочитанных сообщений от человека. |
blocked_bot | true, если человек остановил бота. Отправка ему вернёт 409. |
external_ids | Ваши идентификаторы, привязанные к этому контакту. Массив. |
created_at, updated_at | Когда контакт появился и когда была последняя активность. |
Постраничный обход
total — общее число контактов по текущему фильтру, поэтому обойти базу можно простым циклом:
curl -s "https://etochat.bot/api/v2/{channel}/chats?limit=100&offset=0" -H "Authorization: Bearer etchtbt_bot_ВАШ_КЛЮЧ"
curl -s "https://etochat.bot/api/v2/{channel}/chats?limit=100&offset=100" -H "Authorization: Bearer etchtbt_bot_ВАШ_КЛЮЧ"
Список отсортирован по времени последней активности, а активность идёт постоянно — за время долгого обхода контакты могут переехать между страницами. Если вам важна полнота, ориентируйтесь не на «страницу», а на chat_id: складывайте результат в словарь по нему, тогда сдвиг ничего не испортит.
Поиск
curl -s "https://etochat.bot/api/v2/{channel}/chats?query=Иван" \
-H "Authorization: Bearer etchtbt_bot_ВАШ_КЛЮЧ" \
--get --data-urlencode "query=Иван"
Поиск идёт по имени, фамилии и username, по подстроке. По chat_id и по external_id он не ищет — для них есть отдельные эндпоинты.
GET /{channel}/chats/{chat_id} — карточка одного человека
Когда chat_id уже известен и нужно свежее состояние: не остановил ли человек бота, есть ли непрочитанные, какие внешние id к нему привязаны.
curl -s https://etochat.bot/api/v2/{channel}/chats/9995735656 \
-H "Authorization: Bearer etchtbt_bot_ВАШ_КЛЮЧ"
{
"contact": {
"chat_id": "9995735656",
"name": "Иван",
"username": "ivan",
"status": "open",
"unread": 0,
"blocked_bot": false,
"external_ids": ["user-42"],
"created_at": "2026-07-18 11:04:12",
"updated_at": "2026-07-21 09:31:50"
}
}
Если контакта нет у этого бота — 404 contact_not_found. Если chat_id не похож на число — 400 chat_id_invalid.
Полезный приём перед отправкой: проверьте blocked_bot. Если там true, человек остановил бота, и попытка отправки вернёт 409 recipient_blocked_bot. Лучше сразу пометить это у себя и, например, отправить письмо вместо сообщения в мессенджер.
external_id — ваш собственный идентификатор
В вашей системе человек известен под своим id: user-42, CRM-8891, UUID из базы. Хранить рядом ещё и наш chat_id, следить за его актуальностью, тащить в каждый вызов — лишняя работа.
Поэтому есть привязка: вы один раз сообщаете нам «этот chat_id — это мой пользователь user-42», и дальше адресуете человека своим идентификатором. Наш chat_id можно у себя вообще не хранить.
POST /{channel}/contacts/link — привязать
Запрос
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"
}
Что важно знать про external_id:
- До 190 символов. Длиннее — 400
external_id_too_long. - Регистр сохраняется:
User-42иuser-42— разные идентификаторы. - Он живёт в пределах одного бота, как и
chat_id. - Повторный вызов с тем же
external_idперепривязывает его на новыйchat_id. Это нормальный способ переезда: если человек начал писать боту с другого аккаунта, просто вызовитеlinkещё раз с новымchat_id. - У одного контакта может оказаться несколько ваших идентификаторов — все они видны в поле
external_ids.
Если chat_id в теле запроса не найден у бота — 404 contact_not_found. Если external_id пустой — 400 external_id_required.
Куда подставлять external_id дальше
Везде, где принимается получатель, вместо chat_id можно передать external_id. Отправка сообщения:
curl -s -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":"Ваш тариф продлён до 14 августа."}'
Запуск сценария:
curl -s -X POST https://etochat.bot/api/v2/{channel}/trigger \
-H "Authorization: Bearer etchtbt_bot_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"external_id":"user-42","vars":{"plan":"Премиум","days_left":"3"}}'
Указывать одновременно и chat_id, и external_id не нужно: если передан chat_id, используется он.
Если внешний идентификатор ещё не привязан, вы получите:
{
"error": {
"code": "external_id_not_linked",
"message": "external_id «user-42» не привязан. Сначала вызовите POST /{channel}/contacts/link."
}
}
Типичный порядок работы
- Человек пишет боту. Вы узнаёте его
chat_id— из зеркалирования входящих или изGET /{channel}/chats. - Понимаете, кто это в вашей системе. Например, человек оставил в диалоге почту, или пришёл по ссылке с меткой.
- Вызываете
POST /{channel}/contacts/linkи связываетеchat_idс вашимuser-42. - Дальше во всех вызовах API используете только
external_id.
Шаг 3 достаточно сделать один раз на человека. Повторная привязка того же значения ничего не ломает — она идемпотентна по смыслу и просто перезапишет соответствие.
Ошибки, связанные с адресацией
| Статус | Код | Когда |
|---|---|---|
| 400 | chat_id_invalid | chat_id не число: пустой, с пробелами, обрезанный. |
| 400 | recipient_required | В теле нет ни chat_id, ни external_id. |
| 400 | external_id_required | В POST /{channel}/contacts/link не передан external_id. |
| 400 | external_id_too_long | external_id длиннее 190 символов. |
| 404 | contact_not_found | Такого контакта у этого бота нет — человек не писал боту. |
| 404 | external_id_not_linked | Внешний id не привязан, нужен POST /{channel}/contacts/link. |
| 409 | recipient_blocked_bot | Человек остановил бота, доставка невозможна. |
Все ошибки приходят одинаково: JSON вида {"error":{"code":"…","message":"…"}} с осмысленным HTTP-статусом.
Что дальше
Когда адресация настроена, остаётся собственно действие: отправить сообщение через POST /{channel}/messages или — что чаще правильнее — запустить сценарий через POST /{channel}/trigger, оставив тексты и логику в конструкторе, а из своей системы передавая только переменные.