Тестовый режим¶
Тестовый режим — тот же API, те же URL и тот же контракт, но с ключом test_… и провайдером-песочницей: деньги не двигаются, все объекты помечены test: true, тестовые и боевые данные не смешиваются в списках. Интеграцию нужно полностью отладить в тестовом режиме, затем сменить ключ на live_….
Что умеет песочница¶
Сценарии, которые воспроизводит песочница на чекауте и через simulate:
| Сценарий | Результат |
|---|---|
| Успешная оплата | succeeded, событие payment.succeeded |
| Авторизация без списания | waiting_for_capture для платежей с capture: false; затем capture (в том числе частичный) или cancel |
| Отказ банка | canceled с cancellation_details.reason (insufficient_funds, issuer_declined, general_decline, …) |
| 3-D Secure | страница-имитация подтверждения: успех или отказ three_d_secure_failed |
| Истечение | canceled, expired_on_confirmation (не оплачен) или expired_on_capture (не списан вовремя) |
| Таймаут провайдера | ответ провайдера задерживается; платёж остаётся pending, KASSA дозапрашивает статус |
| Позднее уведомление | уведомление провайдера приходит после истечения платежа — статус не меняется, расхождение фиксируется для сверки |
| Дубль уведомления | повторное уведомление провайдера обрабатывается один раз, событие одно |
| СБП-QR | QR-код с кнопкой «сканировать» на тестовом чекауте |
| Возврат | полный и частичный, refund.succeeded |
Исход выбирают три способа, и все три ведут в одну и ту же таблицу: POST /test/payments/{payment_id}/simulate, метаданные платежа и кнопки на странице-имитации банка (см. ниже).
Что из этого работает в текущей сборке
Список выше работает через API: песочница, POST /test/payments/{payment_id}/simulate, опрос GET /payments/{payment_id}, возвраты (POST /refunds, события refund.*) и доставка вебхуков с подписью. Шаг 4 типового автотеста ниже и пункты чек-листа «сделан возврат» и «подпись вебхука проверена» выполнимы целиком. Страница-имитация банка (3-D Secure, СБП-QR, кнопки исхода) готова и открывается браузером — раздел «Страница-имитация банка» ниже. Не готова сама страница оплаты /c/{token} (этап M1, задача 8): открыть адрес подтверждения можно, а витрины чекаута вокруг него пока нет. 501 not_implemented остаётся у операций поздних этапов — споров (M2) и платёжных ссылок (M5).
Исход по сумме сам не наступает — его нужно запросить
Таблица «сумма → исход» ниже выбирает исход при создании попытки у провайдера, но доводит платёж до него только явный триггер: simulate либо кнопка на странице-имитации банка. Сами, без триггера, наступают ровно два исхода — 01 (отказ банка) и 04 (таймаут провайдера): они происходят в ответе на обращение к провайдеру. Остальные (02, 03, 05, 06, 07 и «остальные — успешная оплата») приходят к нам уведомлением, а отправить его песочнице некому: плановой доставки без запроса в текущей сборке нет (KASSA, вопрос A14.12).
Когда создаётся попытка — зависит от confirmation.type, и это меняет рецепт. При type: qr попытка создаётся вместе с платежом: 01/04 видны сразу в ответе POST /payments, а simulate доступен с первой секунды. При type: redirect (значение по умолчанию) провайдера зовёт выбор способа оплаты на странице оплаты; до него у платежа нет операции у провайдера, и simulate отвечает 422 invalid_state с объяснением. Значит, у redirect-платежа сама по себе не наступает ни одна строка таблицы.
Практический вывод для автотеста: создавайте платёж с confirmation.type = qr и вызывайте simulate с нужным исходом — сумма задаст режим доставки (задержку и дубль), а simulate — судьбу платежа. Для redirect сначала нужен выбор способа на странице оплаты.
Детерминированные исходы без simulate¶
Карточные данные в Merchant API не передаются никогда — ни в тестовом режиме, ни в боевом, — поэтому «тестовой карты» у интеграции с API нет и быть не может. Тот же детерминизм задаётся двумя способами, доступными в тестовом режиме.
Последние две цифры суммы в минорных единицах (копейки, тиын) выбирают исход. Суммы, которых нет в таблице, проходят как обычный успешный платёж — «круглая» тестовая сумма обязана вести себя обычно.
| Последние две цифры | Исход |
|---|---|
01 |
отказ банка (payment.canceled; код отказа по умолчанию general_decline) |
02 |
авторизация без списания → waiting_for_capture |
03 |
непройденная 3-D Secure (three_d_secure_failed) |
04 |
таймаут провайдера — платёж уходит следующему кандидату каскада |
05 |
уведомление приходит с задержкой |
06 |
уведомление приходит дважды (проверка идемпотентности обработчика) |
07 |
платёж истекает по таймауту подтверждения |
| остальные | успешная оплата |
Метаданные платежа имеют приоритет над суммой и позволяют не подгонять копейки:
Ключ metadata |
Значение |
|---|---|
sandbox_outcome |
succeed, decline, authorize, three_ds_fail, provider_timeout, late_webhook, duplicate_webhook, expire |
sandbox_decline_code |
код отказа из справочника песочницы, например card_expired; неизвестный код заменяется общим отказом |
sandbox_delay_seconds |
задержка уведомления в секундах, 0…3600 (по умолчанию 60 для late_webhook) |
Опечатка в этих ключах не ошибка запроса: значение игнорируется, исход выбирается по сумме. Песочница — не то место, где стоит падать на опечатке в метаданных.
Страница-имитация банка¶
Тестовый платёж с confirmation.type = redirect уводит плательщика на страницу-имитацию банка — она живёт на домене API, а не на чекауте, и открывается обычным браузером.
| Адрес | Что там |
|---|---|
{KASSA_SANDBOX_BANK_URL}/sandbox/bank/{provider_payment_id} |
получатель, сумма, кнопки исходов: «оплатить» и три отказа (недостаточно средств, карта истекла, эмитент отклонил) |
.../sandbox/bank/{provider_payment_id}/3ds |
шаг 3-D Secure: поле «код из СМС». Код 123456 — успех, любой другой — отказ three_d_secure_failed. Второй попытки нет |
.../sandbox/bank/{provider_payment_id}/leave |
уйти, не заплатив: исход не назначается, платёж закроется по своему сроку |
{KASSA_SANDBOX_BANK_URL}/sandbox/sbp/{provider_payment_id} |
«приложение банка» для СБП; сюда же ведёт confirmation_data QR-кода, то есть его можно открыть по ссылке, а не только отсканировать |
Адрес страницы приходит в confirmation.confirmation_url попытки, а у СБП — в confirmation.confirmation_data. Идентификатор операции подставлять руками не нужно.
Карточных полей на странице нет вовсе. Номер карты KASSA не принимает нигде и никогда — ни в боевом режиме, ни в тестовом: исход выбирается именованной кнопкой, а не номером. Расхождения между «картой» и «суммой» поэтому не возникает — таблица исходов одна, та же, что выше.
Кнопка возвращает плательщика раньше, чем исход применится. Нажатие ставит задачу, и исход доходит до платежа настоящим подписанным уведомлением — ровно тем же путём, что в бою. Поэтому 303 уводит плательщика на страницу результата до смены статуса: ваша страница «спасибо за покупку» обязана начинать с опроса GET /payments/{payment_id} или ждать события, а не считать возврат по return_url подтверждением оплаты. Настоящий банк ведёт себя так же.
По одной операции решение принимается один раз. Повторная отправка формы по уже завершённой операции отвечает 409 и страницей «операция уже обработана» — как и настоящий банк.
В боевом контуре этих адресов не существует: песочница не обслуживает live, а /sandbox/** закрыт на входящем прокси.
POST /test/payments/{payment_id}/simulate¶
Переводит тестовый платёж в нужный исход без участия плательщика — удобно для автотестов интеграции. Доступно только с тестовым ключом; с боевым ключом запрос отклоняется (403 forbidden).
curl -X POST https://api.kassa.example/v1/test/payments/pay_01J8Z3K4M5N6P7Q8R9S0T1V2W3/simulate \
-u "sh_01J8Z3H4M5N6P7Q8R9S0T1V2W3:test_ru_REPLACE_WITH_YOUR_SECRET" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5e0f1a2b-3c4d-4e5f-9a8b-7c6d5e4f3a21" \
-d '{ "outcome": "succeed", "delay_seconds": 0 }'
Ответ — 202 Accepted без тела: исход применяется асинхронно, результат придёт событием и будет виден в GET /payments/{payment_id}. В автотестах дождитесь события (или опрашивайте статус), а не полагайтесь на немедленное изменение.
Поля запроса:
| Поле | Тип | Описание |
|---|---|---|
outcome |
string, обязательно | исход, см. таблицу ниже |
delay_seconds |
integer 0–300 | задержка применения исхода; удобно для проверки таймеров и состояния «ожидание» |
Исходы (outcome):
outcome |
Из статуса | Что происходит |
|---|---|---|
succeed |
pending, waiting_for_capture |
оплата успешна → succeeded, событие payment.succeeded |
authorize |
pending |
успешная авторизация: платёж с capture: false → waiting_for_capture, событие payment.waiting_for_capture |
decline |
pending |
отказ банка → canceled, party: payment_network, причина из справочника (например issuer_declined) |
expire |
pending, waiting_for_capture |
истечение → canceled, expired_on_confirmation или expired_on_capture |
three_ds_fail |
pending |
плательщик не прошёл 3-D Secure → canceled, three_d_secure_failed |
provider_timeout |
pending |
провайдер не отвечает вовремя; платёж остаётся pending, статус уточняется опросом — проверьте, что ваша интеграция не считает таймаут отказом |
late_webhook |
canceled (истёкший) |
уведомление провайдера об оплате приходит после истечения — статус не «воскресает», расхождение попадает в сверку |
duplicate_webhook |
succeeded |
повторное уведомление провайдера — no-op, дублей событий нет |
Несовместимый с текущим статусом исход → 422 invalid_state. Платёж с test: false или чужой платёж → 404 not_found.
Типовой автотест интеграции¶
POST /paymentsсtest_-ключом иconfirmation.type = qr→201,status: pending. Тип важен: приredirectпопытка у провайдера появляется только после выбора способа на странице оплаты, и шаг 2 ответит422 invalid_state.POST /test/payments/{payment_id}/simulate{ "outcome": "succeed" }→202.- Дождаться
payment.succeededна тестовом адресе вебхука (проверить подпись по контрольному примеру) илиGET /payments/{payment_id}→status: succeeded. POST /refundsна часть суммы →201; дождатьсяrefund.succeeded.- Повторить шаг 1 с тем же
Idempotency-Keyи телом →200и тот же объект; с другим телом →409 idempotency_conflict. - Второй платёж →
simulate{ "outcome": "decline" }→ событиеpayment.canceledсcancellation_details.
Тестовые вебхуки¶
- В кабинете на странице адреса вебхука есть кнопка «Отправить тестовое событие»: KASSA отправит подписанное событие выбранного типа с
"test": true. - Журнал доставок (код ответа, время, число попыток) — в кабинете и в поле
deliveryобъекта события (GET /events). - Проверить свою реализацию подписи без сети можно по контрольному примеру.
Чек-лист выхода в боевой режим¶
Кабинет отмечает пункты автоматически по фактам в тестовом режиме:
- [ ] создан платёж;
- [ ] обработано событие
payment.succeeded(ответ2xx); - [ ] обработано событие
payment.canceled; - [ ] сделан возврат;
- [ ] подпись вебхука проверена (хотя бы одна доставка с верной подписью на ваш адрес);
- [ ] настроен
return_url— публичный адрес поhttps(в песочнице допустим и локальный, см. ниже); на боевом ключе локальный адрес будет отклонён400 invalid_request.
Адрес возврата в песочнице¶
Тестовый ключ принимает return_url, указывающий на вашу машину: https://localhost:3000/return,
https://192.168.1.10/return — разработка идёт локально, и требовать публичный домен раньше, чем
появился сайт, было бы бессмысленно. Боевой ключ такие адреса отклоняет 400 invalid_request с
parameter: confirmation.return_url.
Что не принимается ни в одном контуре: не-https (включая javascript: и data:), учётные
данные в адресе (https://user:pass@…), пробелы и управляющие символы внутри адреса. Перед выкаткой
на боевой ключ замените адрес возврата на публичный — это отдельный пункт чек-листа выше.
После проверки магазина выпустите live_-ключ и замените его в конфигурации. Пока магазин не активирован, запросы с live_-ключом отвечают 422 shop_not_live. Комиссия в песочнице (fee_amount) рассчитывается по демонстрационному тарифу и не равна боевой.