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

Чем KASSA отличается от привычной схемы

KASSA работает по агентской модели (в документах проекта — «модель A»): получателем платежа перед банком выступает касса, а ваш магазин — субмерчант, зарегистрированный кассой в банке. Проверить, что это ваш случай, можно одним запросом — GET /v1/shop вернёт "settlement_model": "agent" (поле только для чтения; вторая модель, technology, включается настройкой юрлица-эквайера, а не запросом).

Для кода интеграции отличий пять. Все они уже описаны в контракте полями и кодами ошибок — ниже сказано, какими именно.

Привычная схема KASSA (модель A)
Получатель платежа магазин касса, магазин — субмерчант
Возврат всегда проходит, пока не превышена сумма платежа оплачивается вашими деньгами и может быть отклонён по балансу
Выплаты идут по расписанию приостанавливаются автоматически при отрицательном балансе и по риск-правилам
Строка в выписке плательщика задаёт магазин формирует касса, в запросе не принимается
Чек пробивает магазин по умолчанию пробивает касса как агент, с вашими реквизитами поставщика

1. Деньги приходят на счёт кассы, а вам — расчётом

Плательщик платит кассе, касса ведёт ваш баланс и выплачивает его расчётами. Отсюда — состав GET /v1/balance: это не одно число, а разложение по состояниям, уровня мерчанта (по ключу любого вашего магазина значения одинаковы), по одной записи на валюту.

{
  "items": [
    {
      "currency": "RUB",
      "available": { "value": "128400.00", "currency": "RUB" },
      "pending": { "value": "36000.00", "currency": "RUB" },
      "reserve": { "value": "14200.00", "currency": "RUB" },
      "deposit": { "value": "0.00", "currency": "RUB" },
      "pending_refunds": { "value": "1500.00", "currency": "RUB" },
      "disputed": { "value": "0.00", "currency": "RUB" },
      "payable": { "value": "128400.00", "currency": "RUB" },
      "test": true
    }
  ]
}
Поле Что это
pending деньги ждут клиринга банка; попадут в available после релиза (hold_days тарифа)
available доступно к отбору в реестр выплат. Уже уменьшено на резерв, возвраты в пути и удержания по спорам — не вычитайте их повторно
reserve удержано rolling reserve; действующая политика — Shop.settlement.reserve по этой валюте
deposit внесённый или удержанный из выплат депозит
pending_refunds возвраты, уже списанные с available, но ещё не подтверждённые провайдером
disputed удержано по открытым спорам
payable попадёт в ближайший реестр выплат; 0.00 при заморозке. Всегда неотрицательно

availableединственное поле баланса, которое может быть отрицательным: это долг перед кассой после чарджбэка или возврата, и он приходит со знаком минус ("-1500.00"). Строгий клиент, сгенерированный из спецификации, к этому готов: у Balance.available и Settlement.net_amount тип SignedAmount, у всех остальных сумм — беззнаковый Amount. Не считайте минус ошибкой ответа.

Каскад покрытия, резерв, депозит и реестр выплат работают строго в пределах одной валюты: долг в RUB не гасится остатком в KZT.

2. Возврат оплачивается вашими деньгами и может быть отклонён

Возврат делает касса из ваших денег, поэтому у POST /v1/refunds есть два отказа, которых нет в схеме «магазин — получатель платежа».

422 refund_exceeds_captured — сумма больше, чем Payment.refundable_amount:

refundable_amount = captured − refunded − charged_back − pending_refunds − disputed

Отказ возможен и при refunded_amount: "0.00" — когда остаток занят чарджбэком, возвратом в пути или открытым спором. Перед возвратом смотрите на refundable_amount, а не на captured_amount.

422 insufficient_merchant_balance — денег не хватает. Сумму возврата должны покрыть вместе:

  • ещё не выпущенная часть этого же платежа (если он не прошёл клиринг, деньги лежат в pending и возвращаются из него всегда — это не расход вашей защиты), и
  • положительный остаток available.

Резерв и депозит на обычный возврат не расходуются, и баланс не уходит в минус автоматически: и то и другое — защита кассы, включить их может только оператор по заявке, с указанием причины. По спорам и чарджбэкам порядок другой — там каскад применяется целиком (см. Споры).

Практический вывод для нового магазина с hold_days > 0: возврат в первый день работы проходит за счёт денег того же платежа, а не за счёт нулевого available. А вот возврат по старому платежу при пустом балансе получит 422 insufficient_merchant_balance — держите на балансе запас под возвраты.

3. Выплаты замораживаются автоматически

Заморозка — не ручное решение поддержки, а состояние счёта. Оно приходит в Shop.settlement.status и отдельными событиями.

{
  "payouts_frozen": true,
  "frozen_reason": "negative_balance",
  "frozen_at": "2026-09-10T11:00:00Z",
  "settlement_delay_days": 1
}
frozen_reason Причина
negative_balance отрицательный баланс после чарджбэка или возврата
chargeback_ratio доля чарджбэков выше порога
refund_ratio доля возвратов выше порога
volume_spike всплеск оборота
manual_review решение оператора
verification проверка данных с банком

Что это значит на практике:

  • реестр выплат не формируется ни по расписанию, ни вручную; Balance.payable в этом состоянии равен 0.00, а next_settlement_at не возвращается;
  • приём платежей продолжается: заморозка выплат и заморозка приёма — разные вещи. Отказ на создании платежа с кодом 422 payments_suspended означает именно остановку приёма, и его причина другая;
  • переход в оба состояния приходит событиями settlement.frozen и settlement.unfrozen с объектом settlement_status (у него нет поля id — разбирайте события по data.object_type, см. вебхуки);
  • события уровня мерчанта приходят на адреса всех ваших магазинов, подписанные на этот тип.

Подпишитесь на settlement.frozen до боевого запуска: без него первым признаком заморозки станет неприход денег.

4. Название и строку в выписке задаёт касса

Payment.merchant_display_name (название магазина, которое видит плательщик на чекауте) и Payment.statement_descriptor (строка в выписке вида «бренд кассы + короткое имя магазина») — поля только для чтения. Касса формирует их из настроек магазина в момент создания платежа и задним числом не меняет.

Поля запроса PaymentCreate.statement_descriptor в контракте нет: раньше оно принималось и молча игнорировалось, а это худший вариант из возможных. Если вы переезжаете с агрегатора, где дескриптор передавался в запросе, — удалите поле из своего кода и читайте фактическое значение из ответа.

Формат ограничен возможностями банка — длиной (не более 32 символов) и допустимым алфавитом. Короткое имя магазина для дескриптора задаётся при онбординге и меняется оператором с записью в аудит — напишите в поддержку, если плательщики не узнают списание: непонятный дескриптор напрямую повышает число споров.

5. Чек пробивает касса как агент

Режим фискализации — настройка магазина Shop.receipt_mode, а не поле запроса.

receipt_mode Кто пробивает чек
agent (по умолчанию) касса через свой ККТ-сервис, с признаком агента и реквизитами поставщика из ваших данных KYB
merchant_issues магазин своей ККТ; касса хранит объект чека для сверки
none чек не требуется по закону региона; включается оператором с указанием основания и записью в аудит

Объект receipt вы передаёте как обычно — из него берутся позиции. Но в режиме agent значения receipt.items[].supplier не применяются: реквизиты поставщика подставляет касса. Поля запроса Receipt.send_by_kassa в контракте нет — фискализацию нельзя выключить одним булевым значением. Статус фискализации виден в Payment.receipt_status; непробитый чек в режиме agent — инцидент кассы, а не ваш, и статус платежа он не блокирует.

Модуль фискализации ещё не включён

Правила режима agent описаны заранее и после включения модуля не изменятся, но в этой сборке его нет: Shop.capabilities.receipts всегда false, непустой receipt отклоняется 422 receipt_invalid с parameter: receipt, Payment.receipt_status не заполняется. Подробности — во врезке быстрого старта.

Что проверить в своей интеграции

  1. Читаете refundable_amount перед возвратом, а не считаете captured − refunded сами.
  2. Обрабатываете 422 insufficient_merchant_balance отдельно от 422 refund_exceeds_captured: первое лечится пополнением баланса, второе — уменьшением суммы или ожиданием исхода спора.
  3. Допускаете минус в Balance.available и Settlement.net_amount (тип SignedAmount).
  4. Подписаны на settlement.frozen, settlement.unfrozen, dispute.opened, dispute.evidence_required.
  5. Не передаёте statement_descriptor при создании платежа и не пытаетесь отключить чек полем запроса.
  6. Разбираете события по data.object_type, а не по префиксу идентификатора.

Коды ошибок целиком — в справочнике ошибок, поля и схемы — в Reference.