Коротко
Песочница (ключ qp_test_…) — полноценный режим, в котором не двигаются реальные деньги и не вызывается Kaspi. Оплату вы «проводите» сами через POST /api/v1/invoices/{id}/simulate. Кассир Kaspi для песочницы не нужен, а счета песочницы не расходуют лимит тарифа.
Перед выходом в прод прогоните минимум шесть сценариев: успешная оплата, отмена, истечение срока, полный возврат, частичный возврат и поздняя оплата. Последний обычно забывают — и именно он потом ломает боевую работу.
Подготовка
- Переключите режим организации в песочницу
- Создайте ключ
qp_test_…и выдайте нужные scope - Добавьте адрес вебхука и сохраните секрет (показывается один раз)
Чем режимы отличаются: Чем песочница отличается от боевого режима.
Шесть основных сценариев
Эндпоинт simulate принимает три статуса: paid, failed, expired. Отмена делается отдельным эндпоинтом, возврат — тоже.
| № | Сценарий | Как воспроизвести | Что проверяете |
|---|---|---|---|
| 1 | Успешная оплата | Создать счёт → simulate {"status":"paid"} | Пришёл invoice.paid, заказ помечен оплаченным, товар/услуга выданы |
| 2 | Отмена | Создать счёт → POST /invoices/{id}/cancel | Статус cancelled, пришёл invoice.cancelled, заказ закрыт |
| 3 | Истечение срока | Создать счёт → simulate {"status":"expired"} | Статус expired, пришёл invoice.expired, резерв товара снят |
| 4 | Полный возврат | Оплаченный счёт → POST /invoices/{id}/refund без суммы | Статус refunded, пришли refund.done и invoice.refunded |
| 5 | Частичный возврат | Оплаченный счёт → refund {"amount": половина} | Статус partially_refunded, остаток посчитан верно |
| 6 | Поздняя оплата | Закрыть счёт через cancel или simulate expired, затем simulate {"status":"paid"} | Событие invoice.paid пришло с признаком late: true — видно, как система обрабатывает оплату уже закрытого заказа |
Шестой сценарий — самый важный. В жизни клиент сканирует QR, думает и подтверждает оплату уже после того, как счёт закрылся. Что делает ваша система: выдаёт услугу или автоматически возвращает деньги? Решите заранее, иначе первый такой случай придётся разбирать руками. Подробно: Поздняя оплата.
Полное описание симуляции: Симуляция оплаты в песочнице.
Как тестировать вебхуки
Вебхук — самая хрупкая часть интеграции, потому что это входящий запрос на ваш сервер. Есть три подхода.
1. Локальный сервер плюс туннель. Удобно во время разработки: туннель (вроде ngrok) выставляет ваш локальный порт наружу. В песочнице это работает.
Но в боевом режиме туннельный адрес не принимается — вы получите webhook_url_tunnel_forbidden. В проде допустим только постоянный домен на https. Используйте туннель на этапе разработки и не оставляйте его как боевой адрес.
2. Внешний приёмник (вроде webhook.site). Хорош, чтобы своими глазами увидеть заголовки, тело и подпись. Но не кладите туда свой секрет — пользуйтесь только секретом песочницы и только для просмотра.
3. Офлайн-проверка подписи. Самый надёжный вариант. Сохраните пришедшее тело в файл и напишите юнит-тест: посчитайте HMAC-SHA256 от строки timestamp + "." + rawBody и сравните с X-Webhook-Signature.
Что обязано делать ваше обработчик вебхука:
- Подпись не сошлась — вернуть 401 и ничего не обрабатывать
X-Webhook-Timestampстарше 5 минут — не принимать- Подпись верна — сначала вернуть 200, тяжёлую работу делать после
- Та же пара
(invoice.id, status)пришла повторно — не обрабатывать второй раз
Если вы не отвечаете 2xx, доставка повторяется 11 раз (с нарастающей паузой от 10 секунд до 1 часа). При длительной череде ошибок адрес временно ставится на паузу. Поэтому порядок «ответить 200, обработать потом» имеет значение.
Детали: Безопасность вебхуков, Настройка вебхуков.
Граничные случаи
Когда основные сценарии пройдены, проверьте вот это — именно оно валит интеграции в проде.
| Что проверяем | Как | Ожидаемый результат |
|---|---|---|
| Дубль запроса | Создайте счёт дважды с одним Idempotency-Key | Новый счёт не создаётся, HTTP 200 и idempotentReplay: true |
| Один заказ дважды | Попробуйте два счёта с одним externalOrderId | Решите заранее, как обрабатываете это на своей стороне |
| Неверная сумма | 0, отрицательное число, строка, тиыны в счёте phone | invalid_amount, amount_too_small, amount_must_be_whole_tenge |
| Отсутствующий телефон | kind: "phone" без customer.phone или с неверным форматом | invalid_phone |
| Несуществующий счёт | GET по выдуманному id | invoice_not_found, 404 |
| Закрытый счёт | Попробуйте отменить оплаченный счёт | invoice_not_open, 409 |
| Возврат больше суммы | Вернуть больше, чем оплачено | invalid_refund_amount |
| Лимиты | Создайте много счетов в песочнице | Счета песочницы не идут в месячный лимит, но частота запросов может дать 429 |
| Битый ключ | Запрос заведомо испорченным ключом | unauthorized, 401 |
| Нехватка прав | Возврат ключом без refunds:write | insufficient_scope, 403 |
Все коды: Каталог ошибок.
Автотесты
Ручная проверка хороша один раз, но повторять её после каждой правки никто не будет. Песочница подходит для автотестов: реальных денег нет, Kaspi не вызывается, лимит не расходуется.
Типовой цикл одного теста:
POST /api/v1/invoices— создать счёт, получитьidPOST /api/v1/invoices/{id}/simulate—{"status":"paid"}- Дождаться вебхука (или прочитать
GET /api/v1/invoices/{id}напрямую) - Убедиться, что статус заказа в вашей базе изменился
- Прибраться за собой
Практические советы:
- Не зависайте в ожидании вебхука. Поставьте таймаут (например, 30 секунд), по истечении прочитайте статус через
GET /api/v1/invoices/{id}и завершите тест по нему - Каждый тест создаёт свой счёт. Тесты, опирающиеся на общий счёт, ломают друг друга
- Генерируйте
Idempotency-Keyслучайно, иначе второй прогон вернёт старый счёт и тест «пройдёт» вхолостую - Тестируйте и ошибочные сценарии. Счастливый путь проверяют все, а падает интеграция на ошибках
- В CI используйте только ключ песочницы. Боевому ключу в тестовом окружении делать нечего
Примеры автотестового цикла есть в документации SDK: Node.js SDK, PHP SDK, Python SDK.
При переходе в боевой режим
Пройденные тесты песочницы не гарантируют, что в проде всё хорошо. Дополнительно проверьте:
- Ключ заменён на
qp_live_…(забывают чаще всего) - Адрес вебхука подключён и для боевого режима
- Кассир активен
- Первый реальный платёж — на минимальную сумму, со своего телефона
Полный список: Чек-лист выхода в прод.
Вопросы и ответы
QR в песочнице настоящий? Нет. Картинка генерируется, но приложение Kaspi её не отсканирует. Оплата проводится через simulate.
Счета песочницы расходуют лимит тарифа? Нет. И в месячный лимит не попадают, и семидневный пробный период не запускают — он начинается с первого боевого счёта.
Можно ли выставить через simulate статус cancelled? Нет, принимаются только paid, failed, expired. Для отмены есть POST /invoices/{id}/cancel.
Что будет, если вызвать simulate боевым ключом? Ошибка not_sandbox, 403. Симуляция доступна только в песочнице.
Нужен ли кассир для тестов? Для песочницы — нет. Кассир Kaspi требуется только для боевых счетов.