Что такое webhooks xpayment, когда они срабатывают, как их настроить и как обрабатывать события платежей и возвратов.
Вебхук — это HTTP-запрос POST, который xpayment отправляет на ваш сервер после перехода платежа или возврата в значимый статус. Эндпоинт может подтвердить заказ, снять резерв или учесть возврат без постоянных запросов к API платежей.
Для прямых платежей и платежных ссылок поток одинаков:
Ваш сервер -> xpayment -> Kaspi
|
новый статус
v
Ваш сервер <- вебхук <- xpayment
xpayment проверяет ожидающие прямые платежи и QR-токены с помощью workers, запускаемых каждые 5 секунд. Отдельный worker проверяет очередь вебхуков каждые 2 секунды. Это интервалы запуска, а не гарантированный срок доставки: на него также влияют ответ Kaspi, нагрузка очереди и сеть.
Используйте вебхуки как основной канал уведомлений. GET /v1/payments/{paymentID} пригодится для сверки или восстановления после недоступности вашего эндпоинта.
Нужны аккаунт xpayment и публичный эндпоинт. В production всегда используйте HTTPS. Сейчас API проверяет только непустой URL, поэтому корректный и доступный HTTPS-адрес должна передать ваша интеграция.
/app/webhooks) и нажмите «Новый вебхук».secret. Технически он необязателен, но пустой secret не обеспечивает аутентификацию.После создания подписка сразу активна. Сохраните secret в менеджере секретов: ответы списка и обновления подписок больше его не возвращают.
Для первичной проверки связи можно использовать временный приемник вроде webhook.site. Не используйте общий тестовый URL или слабый secret в production.
Действие «Тест» отправляет прямо на эндпоинт подписанный пример payment.completed. Оно проверяет связь, но не создает запись в истории доставок и не повторяется. Ответ не-2xx сразу завершает тест ошибкой.
В разделе «Вебхуки» доступны шесть событий платежей и возвратов:
| Событие | Условие в backend | Рекомендуемое действие |
|---|---|---|
payment.completed |
Платеж получил статус COMPLETED |
Один раз выполнить заказ |
payment.expired |
Kaspi вернул CancelledByUser, Expired либо истекший/отброшенный QR-токен |
Снять резерв и предложить новый платеж |
payment.cancelled |
Платеж явно отменен через POST /v1/payments/{paymentID}/cancel либо Kaspi вернул Cancelled |
Снять резерв |
payment.failed |
Платеж перешел в конечный ошибочный статус | Зафиксировать сбой и предложить покупателю новую попытку |
payment.refunded |
Полный или частичный возврат завершен через POST /v1/payments/{paymentID}/refund |
Сверить заказ по payment_id |
payment.refund_failed |
Обработчик не смог завершить возврат | Передать платеж на проверку |
Payload намеренно небольшой. События возврата не содержат refund_id, сумму возврата или причину ошибки. Если они нужны, сопоставьте payment_id со своей записью о возврате или запросите refund API.
Не определяйте действие покупателя только по названию события. В частности, отказ, полученный от Kaspi как CancelledByUser, создает payment.expired, а payment.cancelled означает явную отмену или статус Kaspi Cancelled.
Для всех событий платежей и возвратов используется одна оболочка:
{
"event": "payment.completed",
"event_version": "1",
"delivery_id": "c09a3647-7822-4899-8401-3857cbd5bf68",
"attempt_number": 1,
"source": "xpayment",
"payment_id": "65cd0d84-5f66-4763-94d2-1b8e1ba68347",
"merchant_order_id": "order-5512",
"created_at": "2026-04-17T10:32:00+05:00"
}
| Поле | Значение |
|---|---|
event |
Выбранный тип события |
event_version |
Версия схемы payload; сейчас 1 |
delivery_id |
Постоянный ID этой доставки при всех повторах |
attempt_number |
Начинается с 1 и увеличивается при повторе |
source |
Всегда xpayment |
payment_id |
ID связанного платежа в xpayment |
merchant_order_id |
Ваш ID заказа; отсутствует, если его не передавали |
created_at |
Время постановки события в очередь в формате ISO 8601/RFC 3339 |
Интеграция должна полагаться только на два заголовка:
Content-Type: application/json
X-xPayment-Signature: aeafc0dcd86de4d4d376f525be2b1175520c2818c93938ec8d069957c63a373b
xpayment считает успешным любой ответ 2xx. Сетевая ошибка, HTTP timeout 10 секунд или итоговый ответ не-2xx запускают повтор. Всего выполняется не более трех попыток: после первого и второго сбоев следующие попытки планируются примерно через 1 и 5 минут.
При повторах delivery_id не меняется. Меняется attempt_number, поэтому меняются JSON и его подпись. Возвращайте 2xx только после надежного принятия события; верните не-2xx, если xpayment должен повторить доставку.
Создайте уникальное ограничение по delivery_id и сохраняйте его в одной транзакции с изменением бизнес-данных. В этом концептуальном примере TypeScript обе операции откатятся при ошибке выполнения заказа:
await db.transaction(async (tx) => {
const accepted = await tx.webhookReceipts.insertIfAbsent(payload.delivery_id)
if (!accepted) return // событие уже зафиксировано; отвечаем повторно
await tx.orders.markPaid(payload.merchant_order_id, payload.payment_id)
})
return res.status(200).json({ ok: true })
Если выполнение заказа обращается к внешнему сервису, сохраните outbox-задачу в той же транзакции и выполните ее асинхронно. Если один эндпоинт указан в нескольких подписках, дополнительно обеспечьте единственное выполнение для (payment_id, event): у каждой подписки будет свой delivery_id.
Последние попытки доступны через GET /v1/webhooks/{subscriptionID}/deliveries. Записи имеют статусы PENDING, SUCCESS, FAILED и EXHAUSTED; после последней неудачной попытки доставка становится EXHAUSTED. Используйте эту историю для диагностики, а не вместо сверки в своей системе.
До разбора и обработки payload проверьте X-xPayment-Signature по исходным байтам тела с secret подписки. Алгоритм и примеры для Node.js, Go и Python приведены в статье «Проверка подписи вебхука».