Перейти к содержанию

Быстрый старт

Пять шагов: ключи → создать платёж → отправить плательщика на чекаут → узнать результат → вернуть деньги. Все примеры — для тестового режима; для боевого меняется только ключ.

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_id409 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.

Что дальше