Чем 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:
Отказ возможен и при 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 не заполняется. Подробности — во врезке быстрого старта.
Что проверить в своей интеграции¶
- Читаете
refundable_amountперед возвратом, а не считаетеcaptured − refundedсами. - Обрабатываете
422 insufficient_merchant_balanceотдельно от422 refund_exceeds_captured: первое лечится пополнением баланса, второе — уменьшением суммы или ожиданием исхода спора. - Допускаете минус в
Balance.availableиSettlement.net_amount(типSignedAmount). - Подписаны на
settlement.frozen,settlement.unfrozen,dispute.opened,dispute.evidence_required. - Не передаёте
statement_descriptorпри создании платежа и не пытаетесь отключить чек полем запроса. - Разбираете события по
data.object_type, а не по префиксу идентификатора.
Коды ошибок целиком — в справочнике ошибок, поля и схемы — в Reference.