Коротко
Вебхук — это HTTP-запрос, который мы отправляем на ваш сервер при смене статуса счёта. Чтобы его подключить, зайдите в раздел Интеграции кабинета и добавьте свой адрес: https://qut.kz/app
В боевом режиме адрес должен быть только https и на настоящем домене. IP-адрес, localhost и адреса временных туннелей не принимаются. Секрет показывается один раз — скопируйте его сразу.
Ваш адрес должен быть открыт без авторизации: мы приходим как обычный внешний клиент и не передаём ни логинов, ни паролей.
Как добавить адрес
- Войдите в кабинет: https://qut.kz/app
- Откройте раздел Интеграции.
- Добавьте адрес вебхука — полный путь, например
https://site.kz/qutpay-webhook. - Отметьте нужные события.
- Скопируйте показанный секрет в секретную переменную сервера (например,
QUTPAY_WEBHOOK_SECRET). - Нажмите кнопку проверки и убедитесь, что запрос доходит.
Повторно секрет не показывается. Если потеряли — создайте новый; он начинает действовать с момента создания, поэтому обновите сервер одновременно.
Требования к адресу
| Требование | Боевой режим | Песочница |
|---|---|---|
| Протокол | Только https | Можно http |
| Домен | Нужен настоящий домен | Можно localhost |
| IP-адрес | Не принимается | Можно |
| Адрес туннеля | Не принимается | Можно |
| Авторизация | Не должно быть | Не должно быть |
Если адрес не проходит требования, ошибка появится сразу при сохранении:
| Код | Что случилось |
|---|---|
invalid_url | Адрес некорректен |
webhook_url_requires_https | HTTP-адрес не принимается |
webhook_url_requires_domain | IP или адрес без домена |
webhook_url_tunnel_forbidden | Адрес временного туннеля |
too_many_endpoints | Превышено число вебхуков |
Выбор событий
Включать все события не нужно. Чаще всего достаточно двух: invoice.paid и invoice.refunded.
| Группа | События |
|---|---|
| Счёт | invoice.created, invoice.pending, invoice.paid, invoice.cancelled, invoice.expired, invoice.failed, invoice.lost |
| Возвраты | invoice.refunded, invoice.partially_refunded, refund.done, refund.failed, refund.unknown |
| Общее | invoice.status — один канал на любую смену статуса |
| Ошибки | invoice.create_failed, invoice.cancel_failed, invoice.bulk |
| Подписки | subscription.created, subscription.status |
Какое событие в какой момент приходит: Жизненный цикл счёта.
Заголовки
В каждом запросе приходят четыре заголовка:
| Заголовок | Что это |
|---|---|
X-Webhook-Event | Название события, например invoice.paid |
X-Webhook-Timestamp | Время отправки, Unix-секунды |
X-Webhook-Signature | Префикс sha256= и подпись в hex |
X-Webhook-Delivery | Номер доставки, удобно сверять с журналом |
Подпись обязательно проверяйте: Безопасность вебхуков.
Пример тела
POST /qutpay-webhook HTTP/1.1
Host: site.kz
Content-Type: application/json
X-Webhook-Event: invoice.paid
X-Webhook-Timestamp: 1757167000
X-Webhook-Signature: sha256=3f1a…
X-Webhook-Delivery: 17
{
"event": "invoice.paid",
"invoice": {
"id": "inv_…",
"externalOrderId": "1001",
"status": "paid",
"amount": 2500,
"metadata": { "branch": "almaty-1" },
"receiptUrl": "…",
"paidAt": "…"
},
"sentAt": "…"
}
externalOrderId и metadata — это значения, которые вы передали при создании счёта; они возвращаются без изменений.
Расписание повторов
Если ваш сервер отвечает не 2xx или не отвечает вовсе, мы повторяем запрос 11 раз. Интервал начинается с 10 секунд и растёт до часа.
Из этого следуют два правила:
- Сначала верните 200, работу делайте после. Долгая обработка приводит к таймауту, а таймаут — к повтору.
- Обработка должна быть идемпотентной. Один и тот же статус по счёту может прийти несколько раз. Выполняйте её один раз по паре
(invoice.id, status): Идемпотентность.
app.post('/qutpay-webhook', express.raw({ type: '*/*' }), async (req, res) => {
if (!verify(req)) return res.sendStatus(401);
res.sendStatus(200); // сначала ответ
await enqueue(JSON.parse(req.body)); // потом работа
});
Журнал
В разделе Интеграции кабинета записывается каждая отправка: тип события, время, HTTP-ответ вашего сервера. Любая диагностика начинается отсюда.
| Что в журнале | Значение |
|---|---|
| Нет ни одной записи | Мы не отправляли: адрес не добавлен, событие не выбрано или статус ещё не менялся |
| «Отправлено», ответ 401/403 | Адрес закрыт авторизацией |
| Ответ 404 | Неверный путь или маршрут не зарегистрирован |
| Ответ 301/302 | Редирект — мы его не следуем |
| Ответ 500 | Падает ваш обработчик |
| Таймаут | Обработка слишком долгая |
Полная диагностика: Вебхук не приходит.
Правило редиректов
Редиректы 307 и 308 мы следуем только на сам этот адрес: http → https и вариант со слешем на конце. Редирект, ведущий в другое место, мы не следуем.
Причина простая: слепое следование редиректу может отправить запрос на чужой адрес. Поэтому указывайте в кабинете конечный адрес, а не промежуточный.
Как протестировать
- Отправьте пробный запрос кнопкой проверки в кабинете — так вы увидите, что ваш код проверки подписи работает.
- Перейдите в песочницу, создайте счёт и симулируйте оплату через
POST /api/v1/invoices/{id}/simulate {"status":"paid"}. Вебхуки при этом отправляются по-настоящему. - Чтобы проверить повторы, временно заставьте обработчик отвечать 500 и посмотрите в журнале, как приходят повторные попытки.
Полный сценарий: Как протестировать интеграцию.
Вопросы и ответы
Можно добавить несколько адресов? Да. Например, один на сайт, другой на внутренний мониторинг. У каждого свой набор событий.
Достаточно ли одного вебхука? В большинстве случаев да. Если покупатель стоит у устройства и ждёт результата, опрашивайте статус параллельно: Вебхук или опрос статуса.
Потерял секрет. Создайте новый в кабинете и одновременно замените значение на сервере.
Я сменил адрес — что будет со старыми событиями? События, стоящие в очереди на повтор, уйдут на новый адрес.
В песочнице вебхуки приходят? Да, отправляются по-настоящему. Разница лишь в том, что адрес может быть http и localhost.