API ЭТОЧАТБОТ

Отправка сообщения

Как отправить человеку сообщение через POST /messages: поля запроса, лимиты текста и кнопок, примеры на curl, PHP и Python, разбор ошибок и случаи, когда вместо этого лучше запускать сценарий.

Эндпоинт 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 — текст может меняться.

HTTPcodeЧто произошло
401unauthorizedНе передан заголовок Authorization
401invalid_keyКлюч неверен, отозван или бот отключён
400unsupported_channelКанал бота не поддерживается (Instagram)
400recipient_requiredНе указан ни chat_id, ни external_id
400chat_id_invalidchat_id не похож на идентификатор
400external_id_required / external_id_too_longВнешний id пустой или длиннее 190 символов
400text_required / text_too_longНет текста или он длиннее 4000 символов
402subscription_inactiveПодписка неактивна
404contact_not_foundУ этого бота нет такого контакта
404external_id_not_linkedВнешний id никому не привязан
409recipient_blocked_botЧеловек остановил бота
502send_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 готовить ничего не надо: выпустили ключ и отправляете.


Короткий чек-лист перед первым запросом

  1. Ключ выпущен для нужного бота и канал бота не Instagram.
  2. GET /{channel}/me отвечает 200 и can_send: true.
  3. Получатель уже писал боту, его chat_id взят из GET /{channel}/chats и передаётся строкой.
  4. Текст короче 4000 символов, пользовательские данные в нём экранированы.
  5. В коде разобраны 402, 404, 409 и 502 — и на 409 вы помечаете контакт недоступным, а не повторяете отправку.