Коротко
Когда покупатель оплатил QR, Qut Pay замечает это сам и делает две вещи: присылает на ваш сервер webhook invoice.paid и меняет статус счёта на paid, который можно запросить. Правильная схема: заказ отмечается оплаченным по webhook с проверенной подписью или по ответу GET /invoices/{id} с сервера, но никогда — по тому, что покупатель вернулся на страницу «спасибо».
Способ 1 — webhook (основной)
- В кабинете → Интеграции укажите адрес на вашем сервере и получите секрет — настройка вебхуков.
- При оплате придёт
POSTс JSON{ "event": "invoice.paid", "invoice": { "id", "status": "paid", "amount", "externalOrderId", "paidAt", "receiptUrl", … } }. - Проверьте подпись: заголовок
X-Webhook-Signature: sha256=…— это HMAC-SHA256 от строкиtimestamp + "." + сырое тело, гдеtimestampизX-Webhook-Timestamp. Считайте по сырым байтам тела, до разбора JSON — почему не сходится подпись. - Найдите заказ по
externalOrderId, сверьтеamount, отметьте оплаченным и ответьте200в течение 8 секунд. - Сделайте обработчик идемпотентным: один и тот же
(invoice.id, status)может прийти дважды, повторный должен ничего не менять.
Если сервер не ответил 2xx, Qut Pay повторяет доставку до 11 раз в течение примерно двух часов — вебхук не приходит.
Способ 2 — запрос статуса (дополнительный)
curl https://api.qut.kz/api/v1/invoices/inv_… -H "X-API-Key: qp_live_…"
Ответ содержит status: pending — ждём, paid — оплачен, expired / cancelled / failed — не оплачен. Добавьте ?live=1, чтобы сервис спросил Kaspi прямо сейчас, а не отдал последнее известное состояние. Опрашивать раз в 5–10 секунд, пока покупатель на странице оплаты, нормально; опрос каждую секунду упирается в лимиты запросов. Когда что выбирать: вебхук или опрос.
Страница «спасибо»
После оплаты покупатель возвращается на ваш successUrl. Этот переход — не доказательство оплаты: ссылку можно открыть вручную. На этой странице запросите статус счёта с сервера и показывайте «оплачено» только при paid.
Чего ещё ждать
invoice.expired— QR истёк, покупатель не заплатил; можно создать новый счёт — почему истекает QR.- Оплата после истечения срока — редкий случай, сервис её всё равно находит и присылает
invoice.paidс пометкойlate— запоздавший платёж. - Все статусы и переходы: жизненный цикл счёта.
Проверить без реальных денег
В песочнице (qp_test_…) оплату имитирует запрос POST /invoices/{id}/simulate с телом {"status":"paid"} — webhook приходит настоящий, со всеми заголовками — песочница.