Схема жизненного цикла
Счёт по номеру телефона:
POST /invoices → [processing] → [pending] → [paid] деньги на Kaspi-счёте
│ │ → [cancelled] отменён
│ │ → [expired] истёк (24 ч)
│ └─ вебхук pending / paid / cancelled / expired
└─ (нет вебхука; если не удалось создать → [error] + вебхук error)
QR-счёт создаётся сразу в pending (шага processing нет), а окно на скан у него своё и заметно короче. Терминальный статус ставит Kaspi — ждите вебхук, а не свой отсчёт. Если оплата нужна «когда покупателю будет удобно» — выставляйте счёт по номеру телефона; про окно, qr_expires_at и картинку — «QR-счёт: TTL и лимиты»:
POST /invoices/qr → [pending] → [paid] / [cancelled] / [expired]
└─ pending-вебхука нет; есть событие qr_scanned при скане
Отмена — только для счёта на номер телефона. QR-счёт (is_qr_token: true) отменить нельзя: POST /invoices/{id}/cancel отвечает 409 qr_cancel_unsupported, статус не меняется, QR гаснет сам (см. «QR-счёт: TTL и лимиты»).
Отмена в боевом режиме проходит через техническое cancelling:
POST /invoices/{id}/cancel → [cancelling] → [cancelled] отменён Kaspi, вебхук есть
│ → [pending] мягкий отказ (часто «уже оплачен»), вебхука НЕТ
└────────→ [error] невосстановимая ошибка, вебхук есть
Ветка cancelling → pending вебхука не порождает: реальный статус (обычно paid) принесёт следующий вебхук синхронизации или GET /invoices/{id}. После 202 счёт отменённым не считайте.
Что происходит на каждом шаге
processing — счёт создаётся
Когда вы вызываете POST /invoices, ApiPay сразу отвечает 201 со статусом processing: запрос принят, Kaspi вызывается в фоне. Штатно это секунды. Вебхука на processing нет.
Задержка дольше нескольких минут означает одно из двух: у организации включён «Режим накопления счетов» (Настройки → Организация — счета копятся и уходят в Kaspi пачкой раз в заданное окно) либо выставить счёт в Kaspi не удалось — тогда сервис сам финализирует счёт в error и присылает вебхук. Не пересоздавайте счёт, пока он в processing: получатся два живых счёта, и покупатель может оплатить оба.
pending — ждёт оплаты
Счёт создан в Kaspi и отправлен покупателю. Приходит вебхук invoice.status_changed со статусом pending. С этого момента счёт по номеру живёт 24 часа. У QR-счёта pending наступает сразу при создании, и отдельного pending-вебхука по нему не отправляется.
paid — оплачен, деньги пришли
Покупатель оплатил. Приходит вебхук со статусом paid, деньги поступают напрямую на ваш Kaspi-счёт. Вебхук обычно приходит за 10–30 секунд, в отдельных случаях — до 10 минут; деньги при этом уже у вас, задержка только в уведомлении.
cancelled / expired — не оплачен
cancelled — счёт отменён: вами через API/кабинет или покупателем (закрыл/свернул приложение); у QR-счёта — только покупателем, своей отмены у QR нет. expired — истекли 24 часа, и Kaspi отдал терминальный статус. На оба перехода приходит вебхук.
error — не удалось создать
Если после всех попыток счёт не удалось создать в Kaspi, он финализируется в error с осмысленным кодом причины (например client_not_found — у номера нет Kaspi). Приходит вебхук со статусом error.
«Странные», но законные переходы
Из-за гонок между оплатой и отменой возможны такие последовательности — заложите их в обработчик:
cancelled → paid— вы (или система) отменили счёт, но покупатель успел оплатить на долю секунды раньше. Оплата выигрывает: счёт становитсяpaid, деньги у вас. Придёт корректирующий вебхукpaid.expired → paid— то же самое на границе 24 часов: оплата прошла в последний момент.error → pending— реконсиляция: счёт, отмеченный ошибочным, при сверке с Kaspi оказался живым. Придёт корректирующий вебхук.paid → partially_refunded— по оплаченному счёту сделали частичный возврат. Полного статусаrefundedу счёта не существует — после полного возврата счёт остаётсяpaidс признаком «полностью возвращён».
Прямого перехода error → paid не бывает: ошибочный счёт сначала должен ожить при сверке (error → pending) и только потом может быть оплачен. Не считайте error, cancelled и expired окончательными: обрабатывайте поздний paid.
Какой вебхук на каком переходе
invoice.status_changed— на переходах вpending,paid,cancelled,expired,error,partially_refunded.invoice.qr_scanned— покупатель отсканировал QR (счёт ещёpending, маркерqr_substate: scanned); приходит один раз на QR.invoice.refunded— обработан возврат (completedилиfailed).- Технические
processingиcancellingвебхуков не порождают — на них не завязывайтесь.
Один и тот же переход сервис повторно не отправляет, но дубль доставки всё же возможен — делайте обработчик идемпотентным и дедуплицируйте по паре (invoice.id, invoice.status). Требования к ответу обработчика и правила ретраев — в «Настройке вебхуков».
«Вебхука нет» не значит «оплаты нет»: сверяйте статус через GET /invoices/{id} или раздел «Счета» в кабинете, а причины молчания разбирайте по статье «Вебхук не приходит».
Частые вопросы
Счёт был cancelled, а стал paid — это ошибка?
Нет, это законно: покупатель оплатил на долю секунды раньше отмены, и оплата выиграла гонку. Придёт корректирующий вебхук paid, деньги у вас. То же с expired → paid на границе 24 часов.
Есть ли статус refunded?
Нет. После полного возврата счёт остаётся paid с признаком «полностью возвращён»; после частичного — становится partially_refunded.