Споры и чарджбэки¶
Спор — это требование банка плательщика вернуть деньги по уже проведённому платежу. Инициатор — плательщик или его банк, не вы и не KASSA. В модели A стороной перед банком выступает касса: она получает уведомление, удерживает сумму, собирает от вас доказательства и отправляет их в банк, а результат перевыставляет вам.
Этап M2
Операции споров объявлены в контракте заранее и до наступления этапа M2 отвечают 501 not_implemented с названием этапа в description. Состав полей и коды после заморозки /v1 не изменятся — интеграцию можно писать сейчас.
Виды споров¶
kind |
Что это | Деньги списаны |
|---|---|---|
retrieval |
банк запрашивает документы по операции, не списывая деньги | нет |
chargeback |
чарджбэк: банк возвращает деньги плательщику | да |
complaint |
жалоба плательщика, поданная через банк, до чарджбэка | нет |
Один чарджбэк банка порождает ровно один объект спора, даже если пришёл и уведомлением, и строкой реестра. Повторный цикл по тому же платежу — новый объект спора.
Жизненный цикл¶
opened ──► evidence_required ──► submitted ──► won
│ │ │
│ │ └────────► lost
│ └──────────────────────────► expired
└──────────────────────────────────────────► submitted
status |
Что значит | Что делать |
|---|---|---|
opened |
спор заведён, сумма удержана | подготовить документы; загрузка уже разрешена |
evidence_required |
банк запросил доказательства, идёт срок ответа | загрузить файлы до evidence_deadline_at |
submitted |
оператор KASSA отправил доказательства в банк | ждать решения; загрузка закрыта (422 invalid_state) |
won |
спор выигран, удержание снято | ничего; платёж снова доступен для возврата |
lost |
спор проигран, деньги ушли плательщику | учесть списание, штраф банка и комиссию кассы |
expired |
срок ответа истёк без доказательств | по деньгам равносильно lost |
Загрузка доказательств разрешена в статусах opened и evidence_required и не меняет статус спора: в банк документы отправляет оператор кассы. После успешной загрузки вы по-прежнему видите evidence_required, а не submitted, — это не ошибка.
Дедлайн доказательств¶
evidence_deadline_at — крайний срок вашей загрузки, а не срок ответа кассы банку: касса оставляет себе запас на отправку. Срок приходит вместе с событием dispute.evidence_required и виден в объекте спора.
- После
evidence_deadline_atобе операции загрузки отвечают409 evidence_deadline_passed. Тот же код приходит, когда спор уже переведён вexpired, — у него приоритет над422 invalid_state, чтобы одно состояние не возвращало два разных кода. - Продлить срок нельзя ни через API, ни через поддержку: его задаёт банк.
- Напоминания приходят письмом и баннером в кабинете, повторного события не создаётся:
dispute.evidence_requiredпорождается только переходомopened → evidence_required. Не стройте свои напоминания на повторах этого события — считайте дни отevidence_deadline_at.
Что обычно убеждает банк: подтверждение доставки с подписью получателя, переписка с покупателем, лог доступа к цифровому товару, условия оферты, ранее сделанный возврат. Файлы — PDF, JPEG или PNG, до 10 МБ каждый, не более 10 на спор, комментарий — до 4096 символов.
Загрузка файлов: два шага¶
Merchant API остаётся JSON-only, поэтому файл не передаётся в теле запроса. Сначала вы получаете ссылку для прямой загрузки в хранилище, затем привязываете загруженные файлы к спору.
Шаг 1 — получить ссылку.
curl -X POST https://api.kassa.example/v1/disputes/dsp_01J8Z3P5Q6R7S8T9V0W1X2Y3Z4/evidence/uploads \
-u "sh_01J8Z3H4M5N6P7Q8R9S0T1V2W3:test_ru_REPLACE_WITH_YOUR_SECRET" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7c1e9d2a-4b5f-4a6c-8d90-1e2f3a4b5c6d" \
-d '{
"file_name": "delivery-confirmation.pdf",
"content_type": "application/pdf",
"size_bytes": 348211
}'
Ответ 201 Created:
{
"file_id": "evd_01J8Z3Q6R7S8T9V0W1X2Y3Z4A5",
"upload_url": "https://uploads.kassa.example/evidence/evd_01J8Z3Q6R7S8T9V0W1X2Y3Z4A5?signature=REPLACED",
"upload_expires_at": "2026-09-10T13:00:00Z"
}
Шаг 2 — загрузить файл методом PUT по upload_url, до upload_expires_at. Это прямой запрос в хранилище: без ключа магазина и без Idempotency-Key.
curl -X PUT "https://uploads.kassa.example/evidence/evd_01J8Z3Q6R7S8T9V0W1X2Y3Z4A5?signature=REPLACED" \
-H "Content-Type: application/pdf" \
--data-binary @delivery-confirmation.pdf
Шаг 3 — привязать файлы к спору. Один запрос на все загруженные файлы:
curl -X POST https://api.kassa.example/v1/disputes/dsp_01J8Z3P5Q6R7S8T9V0W1X2Y3Z4/evidence \
-u "sh_01J8Z3H4M5N6P7Q8R9S0T1V2W3:test_ru_REPLACE_WITH_YOUR_SECRET" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3b8d5f1c-6a2e-4c7d-9b0a-5e6f7a8b9c0d" \
-d '{
"file_ids": ["evd_01J8Z3Q6R7S8T9V0W1X2Y3Z4A5"],
"comment": "Товар доставлен 02.09.2026, накладная подписана получателем."
}'
Ответ 201 Created — объект спора с метаданными приложенных файлов:
{
"id": "dsp_01J8Z3P5Q6R7S8T9V0W1X2Y3Z4",
"payment_id": "pay_01J8Z3K4M5N6P7Q8R9S0T1V2W3",
"order_id": "ORD-10042",
"kind": "chargeback",
"status": "evidence_required",
"amount": { "value": "1500.00", "currency": "RUB" },
"bank_fine_amount": { "value": "1200.00", "currency": "RUB" },
"kassa_fee_amount": { "value": "500.00", "currency": "RUB" },
"reason_code": "10.4",
"evidence_deadline_at": "2026-09-17T21:00:00Z",
"evidence": [
{
"file_id": "evd_01J8Z3Q6R7S8T9V0W1X2Y3Z4A5",
"file_name": "delivery-confirmation.pdf",
"content_type": "application/pdf",
"size_bytes": 348211,
"uploaded_at": "2026-09-10T12:05:00Z"
}
],
"created_at": "2026-09-10T11:00:00Z",
"test": true
}
Содержимое загруженных файлов через API не отдаётся — только метаданные. Нарушение лимитов (тип, размер, число файлов, длина комментария, неизвестный file_id) → 400 invalid_request с parameter.
Список споров и карточка читаются обычными запросами; споры видны по ключу того магазина, которому принадлежит платёж:
curl -u "sh_01J8Z3H4M5N6P7Q8R9S0T1V2W3:test_ru_REPLACE_WITH_YOUR_SECRET" \
"https://api.kassa.example/v1/disputes?status=evidence_required&limit=50"
Фильтры: payment_id, status, kind, created_at_gte, created_at_lt; ответ — { "items": [ … ], "next_cursor": … }.
Что происходит с балансом¶
Деньги удерживаются в момент открытия спора, а не в момент решения: если банк спишет сумму, взять её с вас задним числом уже нельзя.
- Удержание. Сумма спора уходит в
Balance.disputedи исчезает изBalance.available. Источники берутся по порядку: сначала ещё не выпущенная часть этого же платежа, затем доступный остаток, затем rolling reserve, затем депозит. Если не хватило и этого —availableуходит в минус, выплаты автоматически замораживаются (frozen_reason: "negative_balance", событиеsettlement.frozen), а приём платежей продолжается. - Возврат по этому платежу запрещён на удержанную сумму. Пока спор открыт,
Payment.refundable_amountуменьшен наdisputed_amount, и попытка вернуть занятое →422 refund_exceeds_captured, даже еслиrefunded_amountравен0.00. Иначе плательщик получил бы деньги дважды — возвратом и по чарджбэку. - Решение.
won— удержание снимается и возвращается туда, откуда бралось, в обратном порядке: депозит → те же партии резерва → ещё не выпущенная часть платежа (она проходит обычный релиз поhold_days, а не становится доступной сразу) → остаток наavailable. Платёж снова доступен для возврата.lostилиexpired— сумма уходит банку:disputed_amountобнуляется,charged_back_amountрастёт на ту же величину. Сумма всех списаний по платежу никогда не превышаетcaptured_amount.
- Штраф банка (
bank_fine_amount) — сумма, которую платёжная система берёт с кассы за сам факт чарджбэка; она перевыставляется вам и в сумму спора не входит. - Комиссия кассы за обработку чарджбэка (
kassa_fee_amount) — по вашему тарифу, сверх штрафа банка. Комиссия за исходный платёж (Payment.fee_amount) при проигранном споре по умолчанию не возвращается: работа по проведению платежа уже выполнена.
Штраф и комиссия списываются тем же порядком источников и, в отличие от суммы спора, суммой платежа не ограничены. В выписке они приходят отдельными колонками bank_fines_amount и chargeback_fees_amount, удержания по спорам — колонкой disputes_amount (GET /settlements).
Состояние удобно проверять двумя запросами: GET /payments/{payment_id} даёт disputed_amount и refundable_amount по конкретному платежу, GET /balance — disputed и available по мерчанту целиком.
События¶
| Событие | Когда | Что делать |
|---|---|---|
dispute.opened |
спор заведён, сумма удержана | завести задачу; найти заказ по order_id |
dispute.evidence_required |
банк запросил доказательства | загрузить файлы до evidence_deadline_at |
dispute.closed |
статус стал won, lost или expired |
зафиксировать исход; при lost учесть списание, штраф и комиссию |
settlement.frozen |
удержание увело баланс в минус | пополнить баланс; выплаты не формируются до снятия заморозки |
Внутри события dispute.* лежит тот же объект Dispute (data.object_type: "dispute"), внутри settlement.frozen — состояние расчётов (settlement_status); как это разбирать — в вебхуках.
Ошибки¶
| HTTP | code |
Когда |
|---|---|---|
| 400 | invalid_request |
тип файла, размер, число файлов, длина комментария, неизвестный file_id |
| 404 | not_found |
спор по платежу другого магазина или несуществующий dispute_id |
| 409 | evidence_deadline_passed |
загрузка после evidence_deadline_at, в том числе в статусе expired |
| 422 | invalid_state |
загрузка в статусе submitted, won или lost |
| 501 | not_implemented |
до наступления этапа M2 |
Полные формулировки — в справочнике ошибок.
Как уменьшить число споров¶
- Понятный дескриптор в выписке: плательщик, не узнавший списание, идёт в банк, а не к вам. Дескриптор формирует касса — см. модель A.
- Быстрый возврат по обращению покупателя дешевле чарджбэка: возврат стоит комиссию, чарджбэк — сумму, штраф банка и комиссию кассы.
- Храните подтверждения доставки и переписку не менее года: спор приходит через недели и месяцы после платежа.
- Подпишитесь на
dispute.openedиdispute.evidence_requiredдо выхода в боевой режим — узнать о споре из выписки означает пропустить срок.