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

Споры и чарджбэки

Спор — это требование банка плательщика вернуть деньги по уже проведённому платежу. Инициатор — плательщик или его банк, не вы и не 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": … }.

Что происходит с балансом

Деньги удерживаются в момент открытия спора, а не в момент решения: если банк спишет сумму, взять её с вас задним числом уже нельзя.

  1. Удержание. Сумма спора уходит в Balance.disputed и исчезает из Balance.available. Источники берутся по порядку: сначала ещё не выпущенная часть этого же платежа, затем доступный остаток, затем rolling reserve, затем депозит. Если не хватило и этого — available уходит в минус, выплаты автоматически замораживаются (frozen_reason: "negative_balance", событие settlement.frozen), а приём платежей продолжается.
  2. Возврат по этому платежу запрещён на удержанную сумму. Пока спор открыт, Payment.refundable_amount уменьшен на disputed_amount, и попытка вернуть занятое → 422 refund_exceeds_captured, даже если refunded_amount равен 0.00. Иначе плательщик получил бы деньги дважды — возвратом и по чарджбэку.
  3. Решение.
    • won — удержание снимается и возвращается туда, откуда бралось, в обратном порядке: депозит → те же партии резерва → ещё не выпущенная часть платежа (она проходит обычный релиз по hold_days, а не становится доступной сразу) → остаток на available. Платёж снова доступен для возврата.
    • lost или expired — сумма уходит банку: disputed_amount обнуляется, charged_back_amount растёт на ту же величину. Сумма всех списаний по платежу никогда не превышает captured_amount.
  4. Штраф банка (bank_fine_amount) — сумма, которую платёжная система берёт с кассы за сам факт чарджбэка; она перевыставляется вам и в сумму спора не входит.
  5. Комиссия кассы за обработку чарджбэка (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 /balancedisputed и 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 до выхода в боевой режим — узнать о споре из выписки означает пропустить срок.