Коротко
Даже после того как счёт перешёл в cancelled или expired, деньги по нему могут прийти с опозданием. Это нормальная, хотя и редкая ситуация. В таком случае к вам приходит событие invoice.paid с меткой late: true. Дальше у вас два пути: оказать услугу или вернуть деньги. Третьего — проигнорировать — нет: деньги лежат на счёте мерчанта, а покупатель ждёт свой товар.
Главное, чтобы ваш код умел ждать такое событие. Многие интеграции обрабатывают invoice.paid только если счёт ещё открыт, и молча теряют поздние оплаты.
Почему так бывает
Счёт живёт и у нас, и в Kaspi, и отсчёт времени с двух сторон не всегда совпадает.
- Покупатель отсканировал QR и долго сидит в окне подтверждения. Мы уже закрыли счёт как
expired, а он всё-таки нажимает «подтвердить». - Связь прервалась, и подтверждение дошло до Kaspi с опозданием.
- Вы отменили счёт ровно в тот момент, когда покупатель платил.
- Обработка на стороне Kaspi ненадолго задержалась.
Kaspi не сообщает точное время оплаты. В payload нет поля «в какую секунду покупатель подтвердил», поэтому измерить задержку по данным невозможно. Вы видите только факт: счёт был закрыт, деньги пришли.
Как это увидеть
| Признак | Где видно |
|---|---|
Событие invoice.paid с меткой late: true | В payload вебхука |
Статус счёта перешёл из закрытого в paid | GET /api/v1/invoices/{id} |
| В Kaspi Pay транзакция есть, а в вашей системе заказ закрыт | При сверке счёта Kaspi со своей базой |
Если вебхуки не слушать, такую оплату вы заметите только в конце месяца при сверке. Поэтому событие invoice.paid стоит обрабатывать всегда.
Готовность в коде
Не считайте позднюю оплату отдельным событием — это обычный invoice.paid, просто с меткой. Главное правило: не привязывайте обработку оплаты к прежнему статусу счёта.
app.post('/webhooks/qutpay', async (req, res) => {
const event = req.headers['x-webhook-event'];
const body = JSON.parse(req.rawBody); // после проверки подписи
if (event === 'invoice.paid') {
const orderId = body.invoice.externalOrderId;
const order = await orders.find(orderId);
// Идемпотентность: обрабатывали ли мы уже этот счёт?
if (order.paidInvoiceId === body.invoice.id) return res.sendStatus(200);
if (body.late) {
// Счёт был закрыт, но деньги пришли
if (order.status === 'cancelled') {
await flagForReview(order, body.invoice); // решает человек
} else {
await fulfil(order, body.invoice); // выдаём услугу
}
} else {
await fulfil(order, body.invoice);
}
}
res.sendStatus(200);
});
Обратите внимание на три вещи:
- Идемпотентность. Обрабатывайте один раз по паре
(invoice.id, status). При ответе не 2xx событие повторится до 11 раз. - Не открывайте закрытый заказ автоматически. Заказ мог быть отменён, а товар продан другому. Такой случай лучше отдать человеку.
- Всегда отвечайте 200. Даже если внутренняя логика не может принять решение, примите событие и положите его в свою очередь.
Как принять решение
| Ситуация | Правильное действие |
|---|---|
| Заказ ещё не выполнен, товар в наличии | Выдайте услугу, отметьте заказ оплаченным |
| Товара нет, заказ закрыт | Верните деньги и сообщите покупателю |
| Покупатель заплатил дважды | Верните лишнее |
| Услуга срочная (подписка, абонемент) | Откройте период с этого момента |
| Мероприятие прошло, билет недействителен | Верните деньги |
Возврат делается через POST /api/v1/invoices/{id}/refund. Если в ответ пришло refund_unknown или refund_pending_unknown, не отправляйте повторно — сначала прочитайте статус счёта, иначе можно вернуть деньги дважды.
Как говорить с покупателем
Поздняя оплата не вина покупателя. Он нажал «подтвердить», деньги ушли — с его точки зрения всё прошло правильно.
- Не тяните с сообщением. Напишите, как только увидели приход.
- Если услугу можно оказать, просто окажите — объяснять ничего не нужно.
- Если нельзя, скажите, что сделан возврат и когда деньги придут.
- Не показывайте покупателю технический текст. Достаточно «оплата обработалась с задержкой».
Чтобы это случалось реже
- Не отменяйте счёт слишком рано. Покупатель может всё ещё быть в окне подтверждения.
- Используйте поле
expiresAt. Не придумывайте собственный срок, опирайтесь на срок самого счёта. - Не закрывайте заказ одновременно со счётом. Лучше подержать его в статусе «ожидает оплату».
- Обязательно слушайте вебхуки. Если смотреть только в кабинет, поздние оплаты проходят мимо.
Если счета не оплачиваются вообще, причина другая: Проблема у вас или у Kaspi.
Вопросы и ответы
Часто ли бывают поздние оплаты? Редко. Но при сотнях счетов в месяц вы с ними столкнётесь, поэтому код стоит подготовить заранее.
Куда приходят деньги, если счёт закрыт? Прямо на ваш счёт Kaspi. Статус счёта на движение денег не влияет.
В каком событии приходит метка late: true? В invoice.paid, если до этого счёт был cancelled или expired.
Если я проигнорирую событие, оно повторится? При ответе не 2xx — да, до 11 раз. Но если вернуть 200 и внутри ничего не сделать, событие потеряется.
Сколько времени есть на возврат? Срок определяется на стороне Kaspi, поэтому тянуть не стоит. Если возврат не проходит, сначала прочитайте статус счёта.