Быстрый старт¶
Пять шагов: ключи → создать платёж → отправить плательщика на чекаут → узнать результат → вернуть деньги. Все примеры — для тестового режима; для боевого меняется только ключ.
1. Ключи и аутентификация¶
В кабинете создайте магазин и API-ключ. Секрет показывается один раз — сохраните его в хранилище секретов вашего приложения; в базе KASSA хранится только хеш, восстановить секрет нельзя (только выпустить новый; старый действует ещё 24 часа для безболезненной ротации).
| Префикс ключа | Режим | Кто проводит платежи | Данные |
|---|---|---|---|
test_ru_…, test_kz_… |
тестовый | песочница KASSA | объекты с test: true, деньги не двигаются |
live_ru_…, live_kz_… |
боевой | банки-партнёры | реальные платежи; доступен после проверки магазина |
Аутентификация — HTTP Basic: имя пользователя — идентификатор магазина shop_id, пароль — secret_key. В curl это флаг -u:
curl -u "sh_01J8Z3H4M5N6P7Q8R9S0T1V2W3:test_ru_REPLACE_WITH_YOUR_SECRET" \
https://api.kassa.example/v1/shop
{
"id": "sh_01J8Z3H4M5N6P7Q8R9S0T1V2W3",
"name": "Демо-магазин",
"region": "ru",
"test": true,
"default_currency": "RUB",
"settlement_model": "agent",
"receipt_mode": "agent",
"settlement": {
"reserve": [
{ "currency": "RUB", "percent_bps": 1000, "days": 180 }
],
"status": {
"payouts_frozen": false,
"settlement_delay_days": 1
}
},
"capabilities": {
"two_stage": true,
"partial_capture": true,
"partial_refund": true,
"receipts": false,
"payouts": false,
"authorize_request_webhook": false
}
}
Ключ привязан к региону (ru/kz): у каждого региона свой контур данных и свои способы оплаты. Отсутствие или неверный ключ → 401 invalid_credentials.
Поля, которые задаёт KASSA, а не магазин (все — только для чтения):
settlement_model: "agent"— расчёты по агентской модели: получатель платежа — касса, магазин — субмерчант. Это меняет поведение возврата, выплат, дескриптора в выписке и чека — Чем KASSA отличается от привычной схемы;receipt_mode: "agent"— чек пробивает касса как агент через свой ККТ-сервис, с признаком агента и реквизитами поставщика из ваших данных KYB (merchant_issues— магазин фискализирует сам,none— чек не требуется по закону региона);settlement.reserve— действующая политика rolling reserve, по одной записи на валюту:percent_bpsв базисных пунктах (1000= 10 %),days— срок удержания партии;settlement.status— состояние расчётов мерчанта:payouts_frozen,settlement_delay_days, а при заморозке —frozen_reasonиfrozen_at. Тот же фрагмент приходит событиямиsettlement.frozenиsettlement.unfrozen. Необязательное полеnext_settlement_atпоявится вместе с реестрами выплат (этап M5); сейчас касса его не заполняет и в ответе его нет — отсутствие необязательного поля не ошибка;capabilities— то, что магазин умеет в этой сборке, а не то, что включено настройкой. Поэтомуreceipts: falseсоседствует сreceipt_mode: "agent": режим чеков магазину задан, а модуль фискализации ещё не включён (см. врезку в §2). Проверяйте возможности поcapabilities, а не по режиму.
Значения раздела settlement и балансы — уровня мерчанта: по ключу любого вашего магазина они одинаковы. Удержанные суммы отдаются по валютам в GET /balance (reserve, deposit, disputed, pending_refunds).
2. Создать платёж¶
POST /payments. Обязательные поля: amount, order_id (уникален в магазине), confirmation. Заголовок Idempotency-Key обязателен.
curl -X POST https://api.kassa.example/v1/payments \
-u "sh_01J8Z3H4M5N6P7Q8R9S0T1V2W3:test_ru_REPLACE_WITH_YOUR_SECRET" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c9a1e-3b7e-4b1f-9c1d-2f0a7d5e8b21" \
-d '{
"amount": { "value": "1500.00", "currency": "RUB" },
"order_id": "ORD-10042",
"description": "Заказ №10042",
"capture": true,
"confirmation": {
"type": "redirect",
"return_url": "https://shop.example/return?order=10042",
"locale": "ru"
},
"customer": { "email": "buyer@example.com" },
"metadata": { "cart_id": "c-77" }
}'
Ответ 201 Created — объект Payment в статусе pending:
{
"id": "pay_01J8Z3K4M5N6P7Q8R9S0T1V2W3",
"status": "pending",
"paid": false,
"refundable": false,
"test": true,
"amount": { "value": "1500.00", "currency": "RUB" },
"captured_amount": { "value": "0.00", "currency": "RUB" },
"refunded_amount": { "value": "0.00", "currency": "RUB" },
"refundable_amount": { "value": "0.00", "currency": "RUB" },
"merchant_display_name": "Демо-магазин",
"statement_descriptor": "KASSA*DEMO-SHOP",
"order_id": "ORD-10042",
"description": "Заказ №10042",
"capture": true,
"confirmation": {
"type": "redirect",
"confirmation_url": "https://pay.kassa.example/c/ck_2Z3K4M5N6P7Q8R9S0T1V2W3X4Y",
"return_url": "https://shop.example/return?order=10042",
"expires_at": "2026-09-11T10:00:00Z"
},
"customer": { "email": "buyer@example.com" },
"metadata": { "cart_id": "c-77" },
"created_at": "2026-09-10T10:00:00Z",
"expires_at": "2026-09-11T10:00:00Z"
}
Что важно:
Idempotency-Key— 8–128 символов, уникальный для каждого нового запроса (рекомендуется UUID v4). Повтор запроса с тем же ключом и тем же телом в течение 24 часов вернёт200и тот же объект — так безопасно повторять запрос после таймаута или обрыва соединения. Тот же ключ с другим телом →409 idempotency_conflict.order_idуникален в пределах магазина; повторное создание с тем жеorder_id→409 duplicate_order_id, вdescription— идентификатор существующего платежа.capture: true(по умолчанию) — списание сразу после авторизации.capture: false— двухстадийная схема (см. ниже).return_url— сюда вернётся плательщик после чекаута. Принимается публичный адрес поhttps: без учётных данных в адресе (https://user:pass@…), без пробелов и управляющих символов, не на непубличный диапазон (10.*,172.16–31.*,192.168.*,127.*,169.254.*,100.64.*), не на внутреннее имя (localhost,*.local,*.internal) и не на IP в нестандартной записи (2130706433,0177.0.0.1,127.1). Нарушение —400 invalid_requestсparameter: confirmation.return_url. В песочнице (ключtest_) ограничение по непубличным адресам снято —https://localhost:3000/returnпринимается, см. тестовый режим. Адрес видит не только ваш сервер: по нему рисуется кнопка возврата в магазин на странице результата KASSA.expires_at— срок жизни платежа (от 5 минут до 7 дней, по умолчанию 24 часа); по истечении платёж переходит вcanceledс причинойexpired_on_confirmation.payment_method: { "type": "sbp" }— ограничить способ оплаты; без него плательщик выбирает способ на чекауте. Доступные способы —GET /payment-methods.merchant_display_nameиstatement_descriptorв ответе формирует касса, а не запрос: первое — название магазина, которое видит плательщик на чекауте, второе — строка в выписке вида «бренд кассы + короткое имя магазина». В запросе эти поля не принимаются, после создания платежа не меняются; почему так — модель A.refundable_amount— остаток, доступный к возврату прямо сейчас (captured − refunded − charged_back − pending_refunds − disputed). Именно на него смотрите перед возвратом, а не наcaptured_amount.- Чек для фискализации передаётся в поле
receipt(профилиru/kz); в режимеreceipt_mode: "agent"реквизиты поставщика подставляет касса, переданные вreceipt.items[].supplierзначения не применяются. Пример — в справочнике API.
Фискализация (FR-RCP) ещё не включена
Поле receipt объявлено в контракте, но модуля фискализации в этой сборке нет: Shop.capabilities.receipts всегда false, а непустой receipt отклоняется — 422 receipt_invalid с parameter: receipt. До включения модуля не передавайте receipt (ни в POST /payments, ни при возврате): состав полей Receipt после заморозки /v1 не изменится, и код, написанный сейчас, останется рабочим.
3. Отправить плательщика на чекаут¶
Перенаправьте плательщика на confirmation.confirmation_url (HTTP 302 или ссылка). На чекауте KASSA он выбирает способ и оплачивает; ввод карты происходит на странице банка — карточные данные никогда не проходят через ваш сервер и через KASSA.
После оплаты (или отказа) плательщик возвращается на return_url; к нему добавляются параметры payment_id и order_id:
https://shop.example/return?order=10042&payment_id=pay_01J8Z3K4M5N6P7Q8R9S0T1V2W3&order_id=ORD-10042
Возврат на return_url — не подтверждение оплаты
Плательщик может закрыть браузер, а может вернуться до того, как банк подтвердил операцию. Отмечайте заказ оплаченным только по событию payment.succeeded (вебхук) или по статусу succeeded из GET /payments/{payment_id}.
4. Узнать результат¶
Основной канал — вебхуки. Дополнительно в любой момент запросите объект:
curl -u "sh_01J8Z3H4M5N6P7Q8R9S0T1V2W3:test_ru_REPLACE_WITH_YOUR_SECRET" \
https://api.kassa.example/v1/payments/pay_01J8Z3K4M5N6P7Q8R9S0T1V2W3
{
"id": "pay_01J8Z3K4M5N6P7Q8R9S0T1V2W3",
"status": "succeeded",
"paid": true,
"refundable": true,
"test": true,
"amount": { "value": "1500.00", "currency": "RUB" },
"captured_amount": { "value": "1500.00", "currency": "RUB" },
"refunded_amount": { "value": "0.00", "currency": "RUB" },
"refundable_amount": { "value": "1500.00", "currency": "RUB" },
"fee_amount": { "value": "45.00", "currency": "RUB" },
"merchant_display_name": "Демо-магазин",
"statement_descriptor": "KASSA*DEMO-SHOP",
"order_id": "ORD-10042",
"description": "Заказ №10042",
"capture": true,
"payment_method": {
"type": "bank_card",
"card": { "first6": "220000", "last4": "0004", "brand": "mir", "issuer_country": "RU" }
},
"customer": { "email": "buyer@example.com" },
"authorization_details": {
"rrn": "000000000001",
"auth_code": "123456",
"three_d_secure": { "applied": true, "challenge_completed": true }
},
"metadata": { "cart_id": "c-77" },
"created_at": "2026-09-10T10:00:00Z",
"expires_at": "2026-09-11T10:00:00Z",
"authorized_at": "2026-09-10T10:01:40Z",
"captured_at": "2026-09-10T10:01:41Z"
}
Если платёж не состоялся — status: "canceled" и причина в cancellation_details (фрагмент ответа):
{
"status": "canceled",
"cancellation_details": {
"party": "payment_network",
"reason": "insufficient_funds"
}
}
Полный список причин (expired_on_confirmation, canceled_by_customer, three_d_secure_failed, issuer_declined, …) — в схеме CancellationDetails справочника.
5. Вернуть деньги¶
Возврат возможен только из статуса succeeded, полностью или частями, пока сумма не превышает refundable_amount платежа:
curl -X POST https://api.kassa.example/v1/refunds \
-u "sh_01J8Z3H4M5N6P7Q8R9S0T1V2W3:test_ru_REPLACE_WITH_YOUR_SECRET" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 0c2d5f7a-8e41-4a6b-9f30-1b2c3d4e5f60" \
-d '{
"payment_id": "pay_01J8Z3K4M5N6P7Q8R9S0T1V2W3",
"amount": { "value": "500.00", "currency": "RUB" },
"reason": "Возврат одной позиции по заявлению покупателя"
}'
Ответ 201 Created — объект Refund:
{
"id": "rf_01J8Z3M7Q8R9S0T1V2W3X4Y5Z6",
"payment_id": "pay_01J8Z3K4M5N6P7Q8R9S0T1V2W3",
"order_id": "ORD-10042",
"status": "pending",
"amount": { "value": "500.00", "currency": "RUB" },
"reason": "Возврат одной позиции по заявлению покупателя",
"test": true,
"created_at": "2026-09-10T12:00:00Z"
}
order_id касса копирует из возвращаемого платежа — по нему события refund.* сопоставляются с заказом тем же полем, что и события платежа (в запросе поле не принимается).
Итог придёт событием refund.succeeded (или refund.canceled); статус также доступен через GET /refunds/{refund_id}. Когда модуль фискализации включён, при частичном возврате передаётся receipt с возвращаемыми позициями; пока он выключен, непустой receipt отклоняется — см. врезку в §2.
Два отказа, которых нет в привычных агрегаторах, — следствие модели A:
422 refund_exceeds_captured— сумма большеrefundable_amount. Это возможно и приrefunded_amount: "0.00", если остаток занят чарджбэком, возвратом в пути или открытым спором (disputed_amount);422 insufficient_merchant_balance— возврат оплачивается вашими деньгами, и их не хватает: сумму не покрывают вместе ещё не выпущенная часть этого же платежа и положительный остатокavailableизGET /balance. Резерв и депозит на обычный возврат не расходуются.
Двухстадийная оплата (холдирование)¶
Создайте платёж с "capture": false. После авторизации он перейдёт в waiting_for_capture (событие payment.waiting_for_capture) — деньги захолдированы на карте плательщика, но не списаны. Затем:
# списать полностью (пустое тело) или частично (amount меньше суммы платежа; остаток холда снимается)
curl -X POST https://api.kassa.example/v1/payments/pay_01J8Z3K4M5N6P7Q8R9S0T1V2W3/capture \
-u "sh_01J8Z3H4M5N6P7Q8R9S0T1V2W3:test_ru_REPLACE_WITH_YOUR_SECRET" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8d4b2c1e-5f6a-4e7b-8c9d-0a1b2c3d4e5f" \
-d '{ "amount": { "value": "1200.00", "currency": "RUB" } }'
# или снять холд
curl -X POST https://api.kassa.example/v1/payments/pay_01J8Z3K4M5N6P7Q8R9S0T1V2W3/cancel \
-u "sh_01J8Z3H4M5N6P7Q8R9S0T1V2W3:test_ru_REPLACE_WITH_YOUR_SECRET" \
-H "Idempotency-Key: 3a1f6c2b-7d8e-4f90-a1b2-c3d4e5f60718"
capture допустим только в статусе waiting_for_capture. cancel допустим в двух статусах: в pending он закрывает чекаут (штатный способ отменить брошенный заказ, не дожидаясь истечения), в waiting_for_capture — снимает холд. В остальных статусах обе операции → 422 invalid_state. Срок холда зависит от банка (обычно до 7 дней), по истечении платёж отменяется с причиной expired_on_capture.
Списки и фильтры¶
curl -u "sh_01J8Z3H4M5N6P7Q8R9S0T1V2W3:test_ru_REPLACE_WITH_YOUR_SECRET" \
"https://api.kassa.example/v1/payments?status=succeeded&created_at_gte=2026-09-01T00:00:00Z&limit=50"
Ответ — { "items": [ … ], "next_cursor": "…" }. Для следующей страницы передайте cursor=<next_cursor> с теми же фильтрами; next_cursor: null — данных больше нет. Фильтры платежей: order_id, status, created_at_gte, created_at_lt.
Что дальше¶
- Настройте вебхуки и проверку подписи — без них интеграция не считается завершённой.
- Прочитайте Чем KASSA отличается от привычной схемы — возврат по балансу, заморозка выплат, дескриптор и чек в агентской модели.
- Пройдите сценарии тестового режима: успех, отказ, истечение, возврат.
- Если ждёте чарджбэки — Споры и чарджбэки: дедлайн доказательств и удержание по спору.
- Держите под рукой справочник ошибок и Reference.