Вебхуки

Вебхуки бота подписок: уведомления о подписках на ваш сервер

Бот подписок может отправлять на ваш сервер HTTP-уведомление каждый раз, когда у пользователя
появляется новая запись подписки или меняется её статус: оформил заказ, оплатил, продлил,
истёк доступ, сработало или не сработало автосписание. Так вы синхронизируете доступы со своей
CRM, начисляете бонусы, ведёте аналитику или подключаете пользователя к сторонним сервисам.

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


1. Как это работает

событие с подпиской (оплата, продление, автосписание, истечение, отказ)
        │
        ▼
   запись подписки создана / сменила статус
        │
        ▼ очередь (обычно 1–3 секунды)
   POST на ваш Webhook URL ──► 2xx — доставлено
                            └► ошибка / нет ответа — повтор (до 3 попыток)
ЧтоКак
Когда срабатываетПри создании записи подписки и при каждом изменении её статуса — для любых источников: покупка в боте, выдача доступа админом в ЛК или через API, автосписание, истечение срока, отказ платёжной системы
Куда идёт запросНа Webhook URL из настроек бота подписок, методом POST
Формат телаapplication/x-www-form-urlencoded (обычная HTML-форма), плоский набор полей
Что считается успехомHTTP-ответ с кодом 2xx. Тело ответа не разбирается
ПовторыДо 3 попыток с паузой 0,5 / 1 с при сетевой ошибке, таймауте или ответе 4xx/5xx
Таймауты5 с на соединение, 20 с на ответ
РедиректыНе выполняются — указывайте конечный адрес
ПорядокНе гарантируется: при быстрой смене статуса два уведомления могут прийти в любом порядке. Ориентируйтесь на status и old_status, а не на порядок получения
ДублиВозможны при повторе после неполученного ответа. Делайте обработку идемпотентной (ключ: subscription_id + event + old_status + status)

2. Настройка в личном кабинете

Бот подписок → Панель управления → Настройки бота подписки → поле «Webhook URL подписок».

ПравилоПояснение
Схемаhttp:// или https:// (рекомендуем https://)
АдресПубличный домен или IP. Локальные и приватные адреса (localhost, 127.0.0.1, 10.x, 192.168.x, 172.16–31.x и т.п.) отклоняются при сохранении
Логин/пароль в URLНе поддерживаются (https://user:pass@host/…). Используйте секрет в пути или query-параметре: https://example.com/hooks/bott?key=…
ВыключитьОчистите поле и сохраните

3. События

eventКогдаold_status
subscription.createdСоздана новая запись подписки. Это происходит при: оформлении заказа пользователем (ещё не оплачен), выдаче доступа администратором, автоматическом продлении (новый период), создании ожидающего платежа автосписания, временном периоде между попытками автосписанияnull
subscription.status_changedУ записи изменился статус (см. раздел 4)предыдущий статус

Одна подписка пользователя — это цепочка записей: каждый оплаченный период, каждая попытка
автосписания — отдельная запись со своим subscription_id. Пользователя и ресурс связывают поля
user_id / telegram_id и resource_id.


4. Статусы записи

statusНазваниеЧто означает
1Не оплаченаЗаказ создан, оплата не поступила. Доступ не выдан
2АктивнаОплачено, доступ действует с paid_at до end_paid_at
3ЗавершенаПериод закончился: истёк срок, открыт новый период, оплата отклонена или автосписание не удалось
4ОжиданиеУстаревший статус, в новых записях не используется

Типовые переходы:

ПереходЧто произошло
1 → 2Оплата прошла, доступ выдан (в том числе успешное автосписание)
2 → 3Доступ закончился или на том же ресурсе открыт новый период (старая запись закрывается)
1 → 3Заказ не оплачен: отказ платёжной системы, ожидаемый платёж автосписания не подтверждён

5. Поля запроса

Все поля приходят строками (особенность x-www-form-urlencoded). Пустые значения (null) передаются пустой строкой.

ПолеТипОписание
eventstringsubscription.created или subscription.status_changed
subscription_idintID записи подписки. Уникален для каждой записи/периода
bot_idintID вашего бота в BOT-T
subscriber_idintID модуля подписок бота (служебное)
resource_idintID группы ресурсов (канал/чат или набор), к которой относится подписка
tariff_idint | nullID тарифа. Пусто, если тариф удалён
user_idintID пользователя в BOT-T (внутренний, не Telegram)
telegram_idint | nullTelegram ID пользователя
statusintТекущий статус записи (раздел 4)
old_statusint | nullПредыдущий статус. Только для subscription.status_changed
paid_atint | nullНачало доступа, Unix timestamp (UTC). Пусто, пока не оплачено
end_paid_atint | nullОкончание доступа, Unix timestamp (UTC)
priceintСумма записи в минимальных единицах валюты (копейки, центы): 99000 = 990,00
currency_idintID валюты в BOT-T (например, 1 — RUB)
balance_type_idint | nullСпособ оплаты / тип баланса: 5 — выдано администратором, 6 — купон, 7 — внутренний баланс, иные значения — платёжные системы. Пусто, пока способ не выбран
item_idint | nullID подключённого способа оплаты (payment_item), если платёж шёл через платёжную систему
is_recurrent0 / 11 — к записи привязано автопродление (сохранён токен рекуррентного платежа)
autopayment_counterint | nullНомер попытки автосписания для записей, созданных логикой автопродления. Пусто для обычных покупок
bot_clone_idint | nullID копии бота, через которую оформлена запись. Пусто — основной бот
created_atintВремя создания записи, Unix timestamp (UTC)

6. Примеры

Пользователь оформил заказ (ещё не оплатил)

POST /hooks/bott HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded

event=subscription.created&subscription_id=501&bot_id=77&subscriber_id=9&resource_id=10
&tariff_id=12&user_id=3345&telegram_id=123456789&status=1&old_status=&paid_at=&end_paid_at=
&price=99000&currency_id=1&balance_type_id=&item_id=&is_recurrent=0&autopayment_counter=
&bot_clone_id=&created_at=1730361600

Оплата прошла, доступ выдан

event=subscription.status_changed&subscription_id=501&bot_id=77&subscriber_id=9&resource_id=10
&tariff_id=12&user_id=3345&telegram_id=123456789&status=2&old_status=1
&paid_at=1730361650&end_paid_at=1732953650&price=99000&currency_id=1&balance_type_id=74
&item_id=15&is_recurrent=1&autopayment_counter=&bot_clone_id=&created_at=1730361600

Срок истёк, автосписание прошло успешно

Придут три уведомления (порядок не гарантирован):

  1. status_changed, subscription_id=501, old_status=2, status=3 — старый период закрыт.
  2. created, subscription_id=640, status=1, autopayment_counter= — создан ожидающий платёж автосписания.
  3. status_changed, subscription_id=640, old_status=1, status=2 — платёж подтверждён, новый период активен.

Автосписание не удалось, назначена повторная попытка

  1. status_changed, subscription_id=640, old_status=1, status=3 — платёж не подтверждён.
  2. created, subscription_id=641, status=1, затем status_changed 1 → 2 с autopayment_counter=1 —
    временный период до следующей попытки (доступ сохраняется, end_paid_at — время следующей попытки).

Когда попытки исчерпаны, последняя запись переходит в 3 без создания новой, и пользователь удаляется из ресурса.

Администратор выдал доступ из ЛК

created (status=1, balance_type_id=) и сразу status_changed 1 → 2 с balance_type_id=5.


7. Как обработать на своей стороне

<?php
// Пример на PHP: минимальный приёмник
if (($_GET['key'] ?? '') !== 'ВАШ_СЕКРЕТ') {
    http_response_code(403);
    exit;
}

$event  = $_POST['event'] ?? '';
$id     = (int)($_POST['subscription_id'] ?? 0);
$status = (int)($_POST['status'] ?? 0);
$old    = $_POST['old_status'] === '' ? null : (int)$_POST['old_status'];

// Идемпотентность: один и тот же переход обрабатываем один раз
$dedupKey = "$id:$event:$old:$status";
if (alreadyProcessed($dedupKey)) {
    http_response_code(200);
    exit;
}

if ($event === 'subscription.status_changed' && $status === 2) {
    grantAccess((int)$_POST['telegram_id'], (int)$_POST['resource_id'], (int)$_POST['end_paid_at']);
}
if ($event === 'subscription.status_changed' && $status === 3 && $old === 2) {
    // Доступ закончился. Проверьте, нет ли у пользователя другой активной записи на этот resource_id,
    // прежде чем отзывать доступ у себя: при продлении старая запись закрывается одновременно с открытием новой.
    maybeRevokeAccess((int)$_POST['telegram_id'], (int)$_POST['resource_id']);
}

markProcessed($dedupKey);
http_response_code(200);

Рекомендации:

  • Отвечайте быстро (до 20 с) и кодом 200. Тяжёлую работу выполняйте после ответа или в фоне.
  • Не доверяйте URL без секрета. Подписи у запросов нет — защищайте адрес случайным ключом в пути или query-параметре и проверяйте его.
  • Не считайте 2 → 3 отзывом доступа само по себе. При продлении старый период закрывается, а новый открывается — у пользователя может быть другая активная запись на тот же resource_id.
  • Храните subscription_id каждой записи: по нему удобно сопоставлять уведомления с историей платежей в ЛК.

8. Где смотреть доставку

Журнал каждой отправки пишется в лог записи подписки (ЛК → Бот подписок → Пользователи → карточка подписки, блок «Лог») и в лог способа оплаты, событие «Webhook подписки»:

РезультатЧто значит
sentДоставлено; в деталях — HTTP-код, номер попытки и первые 2 КБ ответа
failedВсе 3 попытки неудачны; в деталях — текст последней ошибки
bad_urlАдрес не прошёл проверку безопасности в момент отправки (например, домен стал указывать на приватный IP)

9. Частые вопросы

Приходят ли уведомления о рекуррентных (автоматических) платежах?
Да, всегда. Автосписание создаёт новые записи и меняет статусы точно так же, как ручная оплата — вы получите created и status_changed по каждому шагу (раздел 6).

Почему на одно продление приходит несколько уведомлений?
Потому что продление — это закрытие старого периода и открытие нового. Каждое изменение — отдельное событие.

Можно ли получать JSON вместо формы?
Нет, тело всегда application/x-www-form-urlencoded. Любой веб-фреймворк разбирает его как обычную POST-форму.

Уведомление об удалении записи?
Не отправляется. Удаление подписки администратором или отмена неоплаченного заказа пользователем убирает запись без вебхука.

Что если мой сервер был недоступен?
После 3 неудачных попыток уведомление помечается failed и больше не повторяется. Актуальное состояние подписок всегда можно получить через REST API бота подписок.