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

Что такое 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-адрес должна передать ваша интеграция.

  1. Откройте «Вебхуки» в кабинете (/app/webhooks) и нажмите «Новый вебхук».
  2. Выберите готовую интеграцию или вручную укажите URL и необязательное описание.
  3. Выберите нужные события платежей и возвратов. В интерфейсе доступно шесть событий; безопасный вариант по умолчанию — выбрать все.
  4. Задайте стойкий уникальный secret. Технически он необязателен, но пустой secret не обеспечивает аутентификацию.
  5. Сохраните подписку и до запуска отправьте тестовый вебхук.

После создания подписка сразу активна. Сохраните 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.

Payload и заголовки

Для всех событий платежей и возвратов используется одна оболочка:

{
  "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 приведены в статье «Проверка подписи вебхука».

Связанные руководства