Коротко
В песочнице настоящего Kaspi нет, поэтому оплату покупателя вы имитируете сами:
POST /api/v1/invoices/{id}/simulate
{ "status": "paid" }
Дальше счёт ведёт себя как при реальной оплате: меняется статус, присваивается номер чека, пишутся события и вебхук уходит точно так же, как в бою. Это позволяет прогнать интеграцию целиком, не рискуя ни одним тенге.
Работает только в песочнице. В боевом режиме вернётся ошибка not_sandbox (HTTP 403).
В какие статусы можно перевести
status | Что произойдёт |
|---|---|
paid | Счёт помечается оплаченным, выдаётся номер чека, уходит invoice.paid |
failed | Счёт закрывается как неоплаченный, уходит invoice.failed |
expired | Счёт закрывается по сроку, уходит invoice.expired |
Любое другое значение даст invalid_status (HTTP 422).
Перевод применяется к открытому счёту (new, pending). Повторная симуляция по уже закрытому счёту вернёт invoice_not_open (HTTP 409).
Есть одно исключение: счёт в статусе cancelled или expired можно перевести в paid. Это намеренное воспроизведение поздней оплаты — ситуации, когда счёт уже закрыт, а деньги пришли позже. Событие тогда приходит с признаком late: true, ровно как в продакшене. Так и проверяют, что обработчик встречает этот случай правильно: Поздняя оплата.
Что нужно
- Организация должна быть в режиме песочницы (переключается в кабинете);
- ключ должен быть вида
qp_test_…; - у ключа должно быть право
invoices:write.
Кассир Kaspi для песочницы не требуется: Чем песочница отличается от боевого режима.
Kaspi при этом не вызывается вообще. Никаких SMS, настоящих QR и движения денег. Счета песочницы не входят в месячный лимит тарифа и не запускают пробный период.
Как сделать вручную
Есть три способа, результат одинаковый.
Через API:
curl -X POST https://api.qut.kz/api/v1/invoices/inv_…/simulate \
-H 'X-API-Key: qp_test_…' \
-H 'Content-Type: application/json' \
-d '{"status":"paid"}'
В ответ приходит сам обновлённый счёт: status, paidAt, receiptNumber, receiptUrl и остальные поля.
Со страницы оплаты: откройте payUrl счёта песочницы — там есть кнопка, имитирующая оплату. Самый быстрый путь при ручной проверке.
Из кабинета: в разделе Счета откройте карточку счёта — у счёта песочницы доступно действие симуляции.
Цикл проверки
Короткий порядок проверки интеграции:
- Создайте счёт.
POST /api/v1/invoicesключомqp_test_…. СохранитеidиpayUrlиз ответа. - Убедитесь, что адрес вебхука зарегистрирован. Кабинет → Интеграции. В песочнице требования к адресу мягче, но он должен быть доступен.
- Сделайте симуляцию.
POST /invoices/{id}/simulateс{"status":"paid"}. - Проверьте, что вебхук дошёл. В своём журнале: пришло ли
invoice.paid, прошла ли проверка подписи, ответили ли вы 2xx. - Перечитайте счёт.
GET /invoices/{id}— должен бытьstatus: "paid"и заполненныйreceiptNumber. - Повторите для остальных веток: новый счёт →
failed, ещё один →expired, ещё один отмените и затем переведите вpaid(поздняя оплата).
Если вебхук не приходит, причина обычно не в симуляции: Вебхук не приходит.
Автотесты
Симуляция и сделана ради автотестов. Каркас одного теста:
const API = 'https://api.qut.kz/api/v1';
const H = { 'X-API-Key': process.env.QUTPAY_TEST_KEY, 'Content-Type': 'application/json' };
// 1. счёт
const inv = await (await fetch(`${API}/invoices`, {
method: 'POST', headers: { ...H, 'Idempotency-Key': `test-${Date.now()}` },
body: JSON.stringify({ amount: 100, description: 'Тест', externalOrderId: 'T-1' }),
})).json();
// 2. имитируем оплату
await fetch(`${API}/invoices/${inv.id}/simulate`, {
method: 'POST', headers: H, body: JSON.stringify({ status: 'paid' }),
});
// 3. проверяем результат
const after = await (await fetch(`${API}/invoices/${inv.id}`, { headers: H })).json();
if (after.status !== 'paid') throw new Error(`ожидался paid, пришёл ${after.status}`);
Если хотите проверять и вебхук, есть два подхода:
- Напрямую. Тест сам поднимает приёмник (локальный сервер) и ждёт события после симуляции. Учтите, что доставка происходит не мгновенно, — задайте разумный таймаут.
- Косвенно. Вебхук проверяете отдельно, а в тесте гоняете только логику своего обработчика: передаёте ему готовый payload и смотрите, что он делает. Так быстрее и стабильнее.
Чтобы тест был повторяемым, соблюдайте два условия: на каждом прогоне создавайте новый счёт (один и тот же нельзя оплатить дважды) и меняйте значение Idempotency-Key.
Ошибки
| Ошибка | HTTP | Причина |
|---|---|---|
not_sandbox | 403 | Организация в боевом режиме или счёт боевой |
invalid_status | 422 | В status допустимы только paid, failed, expired |
invoice_not_open | 409 | Счёт уже закрыт (и это не случай поздней оплаты в paid) |
invoice_not_found | 404 | Неверный идентификатор или счёт чужой организации |
insufficient_scope | 403 | У ключа нет права invoices:write |
Полный список: Каталог ошибок.
Вопросы и ответы
Можно ли симулировать боевой счёт? Нет, никогда. В боевом режиме эндпоинт возвращает not_sandbox, и это сделано намеренно.
Вебхук после симуляции такой же, как настоящий? Да, полностью: те же имена событий, та же схема подписи, тот же порядок повторов при ответе не-2xx.
Получает ли счёт песочницы чек? Да, номер чека присваивается и страница чека создаётся, но на ней стоит пометка SANDBOX. Фискальный чек при этом не выбивается.
Можно ли симулировать возврат? Отдельная симуляция не нужна: по счёту, доведённому в песочнице до paid, вызывайте обычный возврат.
Подойдёт ли этот цикл для ИИ-агента? Да, именно он удобен агенту для самопроверки: Поручить интеграцию ИИ-агенту.