API ЭТОЧАТБОТ

Ключ доступа: выпуск, хранение, отзыв

Где выпустить ключ к API бота, почему его показывают только один раз, как заменить потерянный, как правильно хранить и чем он отличается от публичного ключа виджета чата на сайте.

Любой запрос к API бота подписывается ключом. Ключ говорит серверу две вещи: кто пришёл и с каким ботом ему разрешено работать. Ничего другого для авторизации не нужно — ни логина, ни отдельного токена, ни OAuth.

Как выглядит ключ

Ключ начинается с префикса etchtbt_bot_, дальше идут 48 шестнадцатеричных символов:

etchtbt_bot_3f9a1c04d7b28e5610af43c9d0e7215b8c4f6a39d2e17b05

Префикс удобен тем, что ключ легко узнать глазами и легко отловить автоматическими сканерами секретов в репозитории.

Как выпустить ключ

  1. Войдите в кабинет и выберите бота, для которого нужен доступ.
  2. Откройте раздел «Для разработчиков».
  3. Перейдите на вкладку «Ключи API».
  4. Нажмите «Выпустить ключ».
  5. Скопируйте показанное значение и сразу положите его туда, где оно будет жить постоянно: переменную окружения на сервере, менеджер секретов, настройки CI. Не в чат, не в заметки, не в письмо.

Сразу после этого проверьте ключ запросом GET /{channel}/me — это самый дешёвый способ убедиться, что всё скопировалось без потерянного символа:

curl -s https://etochat.bot/api/v2/{channel}/me \
  -H "Authorization: Bearer $ETOCHAT_API_KEY"

Ответ:

{
  "bot": {"id": 83, "name": "Бот поддержки", "channel": "webchat", "username": ""},
  "can_send": true
}

Поле can_send: false означает, что подписка на боте неактивна: читать данные вы сможете, а отправка сообщений будет отклонена с ошибкой 402 subscription_inactive. Сам ключ при этом исправен.

Почему ключ показывается только один раз

В нашей базе хранится не ключ, а его хэш (sha256). Когда приходит запрос, мы считаем хэш присланного значения и сравниваем с сохранённым. Обратного пути нет: из хэша исходную строку не восстановить.

Это значит, что никто не может показать вам ключ повторно — ни вы в кабинете, ни поддержка сервиса. И это не неудобство, а свойство, ради которого так сделано: даже если кто-то получит доступ к нашей базе, готовых ключей он там не найдёт.

Практический вывод: момент выпуска — единственная возможность сохранить ключ. Не закрывайте окно, пока не убедились, что значение записано в надёжное место.

Потеряли ключ — что делать

Восстановить нельзя, но заменить можно за минуту:

  1. Выпустите новый ключ на той же странице.
  2. Пропишите его на сервере, перезапустите приложение.
  3. Убедитесь, что интеграция работает на новом ключе (GET /{channel}/me вернул 200).
  4. Отзовите старый ключ.

Порядок именно такой. Если сначала отозвать, а потом выпускать, интеграция полежит в промежутке: все запросы со старым ключом будут отваливаться с 401 invalid_key.

Тот же порядок годится для плановой ротации ключей — она не требует простоя как раз потому, что на боте одновременно могут жить несколько ключей.

Лимит: 5 живых ключей на бота

На одного бота можно держать до пяти неотозванных ключей. Этого достаточно, чтобы:

  • разделить окружения (боевой сервер, тестовый стенд, локальная разработка);
  • дать отдельный ключ подрядчику и отозвать именно его, когда работа закончится;
  • спокойно проводить ротацию: новый ключ уже работает, старый ещё не отозван.

Если лимит исчерпан, кнопка выпуска перестанет работать до тех пор, пока вы не отзовёте что-нибудь ненужное. Ключи, которыми никто не пользуется, лучше отзывать сразу — это уменьшает площадь для утечки.

Ключ привязан к одному боту

Ключ открывает доступ только к тому боту, на котором он выпущен. Даже если у вас в кабинете десять ботов и все они ваши, ключ от бота №83 не видит контакты бота №84 и не может отправить туда сообщение. Нужен доступ к нескольким ботам — выпустите ключ на каждом и храните их по отдельности.

Отсюда же следует полезное правило: ключ можно отдать подрядчику, который делает интеграцию для одного конкретного бота, не выдавая ему доступ ко всему кабинету.

Отдельно про каналы: API работает с чатом на сайте, Telegram, ВКонтакте и МАКС. Instagram не поддерживается из-за правил Meta на исходящие сообщения — ключ от Instagram-бота выпустится, но любой запрос вернёт 400 unsupported_channel. Для Instagram у нас есть отдельное партнёрское API.

Как передавать ключ в запросе

Ключ передаётся заголовком Authorization по схеме Bearer. Никаких параметров в URL, никаких полей в теле — только заголовок.

Authorization: Bearer etchtbt_bot_3f9a1c04d7b28e5610af43c9d0e7215b8c4f6a39d2e17b05

curl

Ключ берём из переменной окружения, чтобы он не осел в истории командной строки:

export ETOCHAT_API_KEY='etchtbt_bot_3f9a1c04d7b28e5610af43c9d0e7215b8c4f6a39d2e17b05'

# проверка ключа
curl -s https://etochat.bot/api/v2/{channel}/me \
  -H "Authorization: Bearer $ETOCHAT_API_KEY"

# отправка сообщения
curl -s https://etochat.bot/api/v2/{channel}/messages \
  -H "Authorization: Bearer $ETOCHAT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"chat_id":"9995735656","text":"Заказ собран, курьер выехал"}'

PHP

<?php

function etochat(string $method, string $path, ?array $payload = null): array
{
    $key = getenv('ETOCHAT_API_KEY');
    if (!$key) {
        throw new RuntimeException('ETOCHAT_API_KEY не задан');
    }

    $headers = [
        'Authorization: Bearer ' . $key,
        'Accept: application/json',
    ];

    $options = [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CUSTOMREQUEST  => $method,
        CURLOPT_TIMEOUT        => 15,
    ];

    if ($payload !== null) {
        $headers[] = 'Content-Type: application/json';
        $options[CURLOPT_POSTFIELDS] = json_encode($payload, JSON_UNESCAPED_UNICODE);
    }

    $options[CURLOPT_HTTPHEADER] = $headers;

    $ch = curl_init('https://etochat.bot/api/v2/{channel}' . $path);
    curl_setopt_array($ch, $options);
    $body   = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    $data = json_decode((string) $body, true) ?: [];

    if ($status >= 400) {
        $code = $data['error']['code'] ?? 'http_' . $status;
        $msg  = $data['error']['message'] ?? 'Неизвестная ошибка';
        throw new RuntimeException("etochat {$code}: {$msg}");
    }

    return $data;
}

// проверка ключа
$me = etochat('GET', '/me');
echo $me['bot']['name'] . ' (' . $me['bot']['channel'] . ')' . PHP_EOL;

// отправка сообщения
etochat('POST', '/messages', [
    'chat_id' => '9995735656',
    'text'    => 'Заказ собран, курьер выехал',
]);

Обратите внимание: chat_id передаётся строкой. У чата на сайте он длиннее 16 цифр, и в JavaScript такое число теряет точность — поэтому мы и отдаём, и принимаем его как строку.

Python

import os
import requests

BASE = "https://etochat.bot/api/v2/{channel}"

session = requests.Session()
session.headers.update({
    "Authorization": f"Bearer {os.environ['ETOCHAT_API_KEY']}",
    "Accept": "application/json",
})


def call(method: str, path: str, payload: dict | None = None) -> dict:
    r = session.request(method, BASE + path, json=payload, timeout=15)
    data = r.json()

    if r.status_code >= 400:
        err = data.get("error", {})
        raise RuntimeError(f"etochat {err.get('code')}: {err.get('message')}")

    return data


# проверка ключа
me = call("GET", "/me")
print(me["bot"]["name"], me["bot"]["channel"], me["can_send"])

# отправка сообщения
call("POST", "/messages", {
    "chat_id": "9995735656",
    "text": "Заказ собран, курьер выехал",
})

Session здесь не для красоты: заголовок с ключом задаётся один раз и дальше уезжает со всеми запросами, так что его негде забыть.

Безопасность

Относитесь к ключу как к паролю от почты компании, а не как к идентификатору приложения.

Что даёт ключ тому, у кого он есть. Полный доступ к переписке бота: выгрузить все контакты со всеми именами, прочитать карточки, отправить любому человеку сообщение от вашего имени, запустить сценарий автоматизации. Утёкший ключ — это не «кто-то узнал номер нашего бота», это «кто-то может писать вашим клиентам».

Где ключ должен жить. Только на сервере: переменные окружения, файл конфигурации вне веб-корня, менеджер секретов, зашифрованные секреты CI. Запросы к API делает ваш бэкенд.

Где ключа быть не должно:

  • в JavaScript на странице, в React/Vue-сборке, в мобильном приложении — всё это отдаётся пользователю, и «спрятать» там ничего нельзя;
  • в публичном репозитории и вообще в коммитах: даже удалённый следующим коммитом ключ остаётся в истории git;
  • в тикетах, чатах, письмах и скриншотах;
  • в логах — если логируете HTTP-запросы, вычищайте заголовок Authorization.

Разделяйте доступы. Тестовому стенду — свой ключ, подрядчику — свой. Тогда отзыв одного ключа не роняет всё остальное.

При малейшем подозрении на утечку — отзывайте немедленно. Отзыв мгновенный: со следующего запроса ключ отдаёт 401 invalid_key. Сначала выпустите замену и раскатите её, потом отзовите скомпрометированный. Если утечка серьёзная и счёт идёт на минуты — отзывайте сразу, а интеграцию поднимете на новом ключе следом.

Ключ можно только отозвать — «приостановить» или «сменить пароль» у него нельзя. Отозванный ключ не восстанавливается.

Чем ключ API отличается от ключа виджета чата на сайте

Их легко перепутать, потому что оба называются «ключом» и оба относятся к одному боту. Но это принципиально разные вещи.

Ключ APIКлюч виджета чата на сайте
Где находитсяна вашем серверепрямо в HTML-коде страницы
Кто его видиттолько вылюбой посетитель сайта
Что им можночитать контакты, писать людям, запускать сценарииоткрыть окно виджета и принять сообщение от посетителя
Можно ли отправлять сообщенияданет
Как передаётсязаголовок Authorization: Bearerв коде подключения виджета

Ключ виджета публичный по замыслу: он обязан лежать в открытом виде на странице, иначе виджет не сможет загрузиться у посетителя. Поэтому за ним не стоит никаких прав — он только помогает понять, чей это виджет, и принять входящее сообщение. Отправить им что-либо от имени бота нельзя.

Ключ API — ровно наоборот: он секретный и может почти всё. Если вы вставите ключ API в код страницы, вы фактически опубликуете доступ к переписке бота.

Практическое правило: если значение видно в исходном коде страницы — это не ключ API. Если это ключ API — верните его на сервер и немедленно отзовите тот, что успел засветиться.

Ошибки, связанные с ключом

Ошибки всегда приходят в одинаковом виде:

{"error": {"code": "invalid_key", "message": "Ключ неверен или отозван"}}
СтатусКодЧто случилось
401unauthorizedзаголовок Authorization вообще не передан
401invalid_keyключ неверный, отозван или бот отключён
400unsupported_channelканал бота не поддерживается (Instagram)
402subscription_inactiveподписка неактивна, отправка невозможна

Если получаете unauthorized, хотя ключ вроде бы передаёте, проверьте типовые причины: потерянное слово Bearer, лишний пробел или перевод строки в конце значения, прокси или CDN, вырезающий заголовок Authorization.

Если получаете invalid_key — сверьте, тот ли ключ подставился (окружения легко перепутать), не отозван ли он в кабинете и включён ли сам бот.


Дальше: как найти chat_id и отправить первое сообщение. И держите в голове главное ограничение API: написать человеку можно только после того, как он сам обратился к боту — до первого обращения адреса доставки не существует ни в одном канале.