Вебхуки
Система отправляет события на URL, зарегистрированные через POST /v1/webhooks (обновление — PUT /v1/webhooks/{id}; при создании обработчик должен ответить 2xx на CHECK_HEALTH). Вебхуки подписываются отдельным секретом, независимым от секрета API-ключа.
Вебхуки — удобный канал уведомлений. Источник достоверного состояния — REST: даже если обработчик пропустит событие, GET /v1/orders?updated_since=… позволяет восстановить актуальное состояние. Учитывайте это при проектировании.
webhook-events.ru.jsonКаталог событий
| event_type | Когда возникает | Основные поля данных |
|---|---|---|
order.completed | Заказ оформлен, eSIM готовы | order_idexternal_refitems[] (order_item_id, iccid) |
order.failed | Заказ не завершён (ошибка выдачи и т. п.) | order_idexternal_refreason |
esim.delivered | Выдана eSIM с данными установки Связь с заказом по `order_id` и `external_ref`. `lpa` расшифровывается непосредственно перед POST и не хранится открытым текстом в доставках. URL изображения QR не передаётся: создайте QR из `lpa` или получите `qr_code_base64` через `GET /v1/esims/{id}`. | order_item_idorder_idexternal_reficcidlpa |
esim.activated | eSIM установлена и активирована (smdp enabled/in_use) Используйте `order_id` и `external_ref` (ваша ссылка, переданная при покупке) для связи eSIM с вашим заказом. | order_item_idorder_idexternal_reficcidactivated_at |
esim.usage_threshold | Расход достиг порога 75% / 90% Свяжите eSIM с заказом через `order_id` и `external_ref`. Передаётся только наибольший новый пройденный порог: рост 0→95% вызывает только 90. `threshold` означает, что превышен как минимум этот уровень. | order_item_idorder_idexternal_refthreshold (75/90)used_bytesdata_bytes |
esim.expiring | За 3 дня / 1 день до истечения срока Используйте `order_id` и `external_ref` (ваша ссылка, переданная при покупке) для связи eSIM с вашим заказом. | order_item_idorder_idexternal_refdays_remaining (3/1)expires_at |
wallet.low_balance | Баланс кошелька достиг порога предупреждения 25% / 10% `threshold` — процент (25/10), передаётся только при известном значении; иначе поле отсутствует (сравнивайте `balance` самостоятельно). Сумма VND никогда не передаётся в `threshold`. | threshold (25/10)balancecurrency |
catalog.updated | Изменение каталога: тариф добавлен, изменён, скрыт, изменена базовая цена или закончился запас Цены не передаются, поскольку они зависят от организации. Получите ваши цены через `GET /v1/catalog/packages`. Изменения за короткое окно (по умолчанию 10 минут) объединяются в один вебхук `changes[]`; каждый тариф появляется один раз. Синхронизация сотен тарифов не создаёт сотни вебхуков. Действует в тестовом и рабочем режимах. | changes[] (package_code, change: price|availability|package, occurred_at)change_count |
event_type: "ping"Каждый `POST /v1/webhooks` и `PUT /v1/webhooks/{id}` проверяет доступность до сохранения обработчика.
Нельзя подписаться или отписаться через `events` (значение `ping` даёт `internal.validation_failed`); событие получает каждый обработчик. Формат и подпись как у реальных событий (`X-TKE-Webhook-Signature`, без `-Old`), используйте для проверки подписи. На неизвестный `event_type` обработчик должен отвечать 2xx: при 4xx создание/изменение завершается `webhook.health_check_failed` (422), ничего не сохраняется. `ping` не входит в `GET /v1/webhooks/deliveries`.
Обработчик должен распознавать этот тип события, иначе регистрация завершится ошибкой.
{
"event_id": "018f3c00-0000-7000-8000-0000000000c1",
"event_type": "ping",
"occurred_at": "2026-07-11T07:29:00Z",
"is_testmode": true,
"data": {
"message": "TongKho eSIM webhook health check"
}
}Формат вебхука и заголовки подписи
Все вебхуки используют единый формат. Подпись `HMAC-SHA256(webhook_secret, timestamp\nraw_body)` в `X-TKE-Webhook-Signature` (hex в нижнем регистре). В течение 24 часов смены секрета также передаётся `X-TKE-Webhook-Signature-Old`. Доставка как минимум один раз; удаляйте дубли по `event_id`.
{
"event_id": "018f3c00-0000-7000-8000-0000000000b0",
"event_type": "order.completed",
"occurred_at": "2026-07-11T07:30:00Z",
"is_testmode": false,
"data": {
"order_id": "018f2b00-0000-7000-8000-00000000009a",
"external_ref": "partner-cart-9931",
"items": [
{
"order_item_id": "018f2b00-0000-7000-8000-000000000001",
"iccid": "8984012345678901234"
}
]
}
}| Поле | Описание |
|---|---|
data | Содержимое зависит от типа события (см. ниже). |
event_id | Постоянный UUID, уникальный для события (защита от дублей). |
event_type | Тип события. |
is_testmode | true, если событие создано тестовым ключом. |
occurred_at | ISO-8601 UTC. |
Синхронизация каталога: catalog.updated
Возникает при добавлении, изменении или скрытии тарифов, смене базовой цены или исчерпании запаса. Данные содержат объединённый массив changes[] : сотни изменений тарифов означают одну доставку на каждый URL.
{
"event_id": "018f3c00-0000-7000-8000-0000000000c1",
"event_type": "catalog.updated",
"occurred_at": "2026-07-11T08:10:00Z",
"is_testmode": false,
"data": {
"changes": [
{
"package_code": "THAY-BANG-MA-GOI-CUA-BAN",
"change": "price",
"occurred_at": "2026-07-11T08:02:11Z"
},
{
"package_code": "VN_10GB_30D",
"change": "availability",
"occurred_at": "2026-07-11T08:04:52Z"
},
{
"package_code": "JP_3GB_7D",
"change": "package",
"occurred_at": "2026-07-11T08:09:30Z"
}
],
"change_count": 3
}
} Цены продажи зависят от организации . Единая цена была бы неверной и раскрывала бы конфиденциальные данные. Это событие служит сигналом : вызовите GET /v1/catalog/packages?market=… для обновления цен.
change принимает три значения: price (изменение базовой цены), availability (наличие или отсутствие запаса), package (добавлен / обновлён / скрыт). Неожиданное значение также считайте сигналом синхронизации.
Подпись, повторы, задержки и защита от дублей по event_id работают как у обычных событий в тестовом и рабочем режимах с разделением по is_testmode.
Проверка подписи вебхука
Каждый исходящий вебхук содержит X-TKE-Webhook-Signature, вычисленный HMAC-SHA256 с webhook_secret по исходным байтам тела запроса.
signing_string = timestamp + "\n" + raw_body # timestamp = header X-TKE-Webhook-Timestamp
signature = HMAC_SHA256(webhook_secret, signing_string) → hex lowercase
# Проверяйте свежесть времени (±300 с) для защиты от повторного воспроизведения.
# Подписывайте точные полученные байты. НЕ сериализуйте JSON повторно.Интервалы повторной доставки
Если обработчик вернул не 2xx или не ответил за 10 секунд, система повторяет доставку с растущей задержкой в течение 24 giờ: 1 phút → 5 phút → 30 phút → 2 giờ → 6 giờ.
4 важных правила работы с вебхуками
Особенности полей и поведения
esim.delivered.lpa расшифровывается перед отправкой POST и не хранится открытым текстом. URL изображения QR не передаётся: создайте QR из LPA или получите через GET /v1/esims/{id}.
esim.usage_threshold: передаёт только наибольший пройденный порог: при росте 0→95% приходит только threshold=90.
wallet.low_balance.threshold является процентом (25/10), а не суммой VND.
catalog.updated не содержит цен и подробного запаса, только код тарифа и тип изменения.