S
docs.syncra.money
API ReferenceMerchant API

Диспуты и Апелляции

Разрешение спорных ситуаций по платежам: создание апелляций, чат с арбитром, полный контракт Appeal Service в Syncra V2

Диспуты и Апелляции (Appeals)

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

Для разрешения таких конфликтов в Syncra V2 предусмотрен сервис апелляций (Appeal Service): мерчант открывает апелляцию по сделке, прикладывает файлы-доказательства и ведёт переписку в чате спора с арбитром платформы.

Все эндпоинты раздела — под HMAC-парой мерчанта; создание апелляции и чат доступны и из кабинета (cabinet-JWT сессия, тот же контракт — инициатор резолвится из аутентификации).


Процедура рассмотрения спора (Workflow)

Загрузка диаграммы...
  1. Инициирование спора. Мерчант создаёт апелляцию, указывая причину и прикладывая файлы-доказательства (UUID предварительно загруженных файлов).
  2. Арбитраж. Арбитры Syncra проверяют подлинность доказательств, сверяют время и реквизиты перевода, после чего выносят решение.

Статусы апелляций

Канонический жизненный цикл — domain.AppealStatus (5 значений). Полная таблица с V1-маппингом — в Справочнике статусов.

Статус V2Финальный?Описание
OPENНетАпелляция создана, ожидает назначения арбитра.
IN_PROGRESSНетАрбитр рассматривает спор, ведётся чат.
REOPENEDНетАпелляция повторно открыта для доп. расследования.
SATISFIEDДаСпор разрешён в пользу инициатора (мерчанта).
REJECTEDДаСпор отклонён — доказательства недостаточны.

Исход резолва для сделки (2026-09-08): SATISFIED завершает живую сделку в COMPLETED; REJECTED переводит сделку через возврат в CANCELLED (терминально) — эскроу-резервы возвращаются полностью (order-level и все per-match холды). Финальный статус сделки приходит отдельным вебхуком (deal.completed / deal.cancelled).

V2 использует только эти 5 canonical значений. Legacy-имена IN_REVIEW, RESOLVED_MERCHANT, RESOLVED_TRADER, CLOSED, REFUNDED в V2 не существуют — они эмулируются только на стороне V1 compat-слоя для мигрирующих мерчантов.


Зачёт просроченного платежа по резолву (Settle-as-Received)

Типичный кейс спора: игрок перевёл меньше заявленного (partial) или перевёл с опозданием — сделка успела истечь в EXPIRED, при этом received_amount > 0. Деньги у трейдера, сделка мертва. Resurrect-режим запрещён: EXPIRED терминален и не меняется никогда. Вместо этого оператор закрывает спор созданием сеттлмента — новой сделки, зачитывающей фактически полученную сумму:

POST /api/v1/p2p/appeals/{appeal_id}/settlement

Это операторское действие кабинета (арбитражная панель). Тело: received_amount_override (необязательно, при ручном уточнении суммы — тогда reason обязательна), reason, idempotency_key (UUID). Полное описание семантики — PayIn → Зачёт просроченного платежа.

Свойства резолва-через-сеттлмент:

  • апелляция резолвится SATISFIED, в resolution_note — ссылка на сделку-сеттлмент (parent_deal_id → истёкший оригинал, origin_appeal_id → апелляция);
  • одна апелляция → максимум один сеттлмент (UNIQUE-констрейнт на уровне БД): повторный вызов возвращает уже созданную запись (replay), а не ошибку;
  • сеттлмент проходит полный стандартный путь (эскроу → verify → complete), курс — новый, на момент резолва;
  • по сеттлменту приходят обычные вебхуки deal.createddeal.completed (V1 и V2); оригинал навсегда остаётся EXPIRED со своим received_amount.

Отказы: родитель не EXPIRED или апелляция уже закрыта → 409; received_amount = 0 без override → 400; override без reason400.


Создание апелляции

POST /api/v1/p2p/merchant/appeals

Параметры запроса (JSON Body)

ПолеТипОбяз.Описание
deal_idstring(UUID)ДаUUID сделки, по которой открывается спор.
reasonstringДаПричина открытия спора — минимум 10 символов (после обрезки пробелов; короче → 400 reason is required (at least 10 characters)). Текст видит арбитр — пишите содержательно.
file_idsstring[]НетМассив UUID предварительно загруженных файлов-доказательств.

Инициатор резолвится из аутентификации (HMAC claims или кабинетный JWT) — поля merchant_id/tenant_id в теле не нужны и advisory. Сделка должна принадлежать вызывающему мерчанту: чужая → 403, несуществующая → 404. По сделке в статусе, допускающем спор (после эскроу-лока), FSM переводит сделку в APPEALED и шлёт вебхук deal.appealed.

Пример запроса

{
  "deal_id": "7ac148fe-19a3-45bb-b992-019fac55b721",
  "reason": "Клиент перевёл 2500 рублей, но платёж не зачислен в течение 30 минут",
  "file_ids": ["f3a1b2c3-4d5e-6f7a-8b9c-0d1e2f3a4b5c"]
}

Пример ответа (200 OK)

{
  "appeal": {
    "appeal_id": "a82d77a0-0d3a-4422-b91c-7cb052a21def",
    "deal_id": "7ac148fe-19a3-45bb-b992-019fac55b721",
    "status": "OPEN",
    "reason": "Клиент перевёл 2500 рублей, но платёж не зачислен в течение 30 минут",
    "resolution_note": "",
    "created_at": "2026-07-12T17:25:00Z",
    "resolved_at": "",
    "updated_at": "2026-07-12T17:25:00Z"
  }
}

Ошибки создания (живые ответы стейджа)

400 INVALID_ARGUMENTreason короче 10 символов:

{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "{\"code\":3,\"message\":\"reason is required (at least 10 characters)\",\"details\":[]}",
  "instance": "/api/v1/p2p-engine"
}

400 FAILED_PRECONDITION — на сделке уже есть активная апелляция (дублей веток нет: с OPEN/IN_PROGRESS/REOPENED повтор запрещён; терминальные SATISFIED/REJECTED не блокируют новую):

{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "{\"code\":9,\"message\":\"deal 359fb026-6dd7-4f7a-bfa7-aa54a5afb6cb already has an active appeal (aa1fac74-33b3-41ce-ae47-445bc3dcd37d, status OPEN) — continue the existing thread\",\"details\":[]}",
  "instance": "/api/v1/p2p-engine"
}

404 NOT_FOUND — несуществующий deal_id (deal not found); 403 PERMISSION_DENIED — сделка другого мерчанта (cannot access another merchant's resources).


Получение информации об апелляции

GET /api/v1/p2p/merchant/appeals/{appeal_id}

Пример ответа (200 OK, живой ответ стейджа)

{
  "appeal": {
    "appeal_id": "aa1fac74-33b3-41ce-ae47-445bc3dcd37d",
    "deal_id": "359fb026-6dd7-4f7a-bfa7-aa54a5afb6cb",
    "status": "OPEN",
    "reason": "E2E merchant appeals full-cycle test: payer claims transfer completed but escrow never confirmed",
    "resolution_note": "",
    "created_at": "2026-08-23T19:04:44Z",
    "resolved_at": "",
    "updated_at": "2026-08-23T19:04:44Z"
  }
}

Чужая апелляция → 403, несуществующая → 404 (appeal not found).


Список апелляций

GET /api/v1/p2p/merchant/appeals

Параметры фильтрации (Query)

ПараметрТипОписание
statusstringФильтр по canonical статусу: OPEN, IN_PROGRESS, REOPENED, SATISFIED, REJECTED.
page_sizeint32Строк на страницу (1–100, по умолчанию 20).
page_tokenstringНепрозрачный keyset-курсор из предыдущего ответа. Пустая строка = первая страница.

Пример ответа (200 OK)

{
  "data": [
    {
      "appeal_id": "aa1fac74-33b3-41ce-ae47-445bc3dcd37d",
      "deal_id": "359fb026-6dd7-4f7a-bfa7-aa54a5afb6cb",
      "status": "OPEN",
      "reason": "E2E merchant appeals full-cycle test: payer claims transfer completed but escrow never confirmed",
      "resolution_note": "",
      "created_at": "2026-08-23T19:04:44Z",
      "resolved_at": "",
      "updated_at": "2026-08-23T19:04:44Z"
    }
  ],
  "next_page_token": ""
}

В текущем релизе листинг пагинирован по канону AIP-158: keyset-курсор (page_size 1–100 с умолчанием 20, next_page_token пуст = последняя страница, битый непустой page_token400 invalid page_token). Листинг скопирован по владельцу: апелляции чужих сделок в ответ не попадают.


Чат апелляции

В рамках апелляции мерчант может обмениваться текстовыми сообщениями с арбитром платформы. Доступно и из кабинета (JWT).

Отправка сообщения в чат

POST /api/v1/p2p/merchant/appeals/{appeal_id}/messages

Параметры запроса

ПолеТипОбяз.Описание
bodystringДаТекст сообщения (непустой — пустое body отклоняется с 400 body is required).
file_idsstring[]НетUUID загруженных файлов-доказательств.

Пример запроса

{
  "body": "Предоставляю выписку из банка для подтверждения списания.",
  "file_ids": []
}

Пример ответа (200 OK, живой ответ стейджа)

{
  "message": {
    "message_id": "a7b598e0-2074-4873-91ee-954ec588bcfc",
    "appeal_id": "aa1fac74-33b3-41ce-ae47-445bc3dcd37d",
    "sender_role": "MERCHANT",
    "body": "Предоставляю выписку из банка для подтверждения списания.",
    "file_ids": [],
    "created_at": "2026-08-23T21:48:24Z"
  }
}

sender_role резолвится из аутентификации (MERCHANT для API/кабинета, ADMIN — сообщения арбитра, TRADER — трейдера).

Получение истории сообщений

GET /api/v1/p2p/merchant/appeals/{appeal_id}/messages

Пример ответа (200 OK)

{
  "data": [
    {
      "message_id": "b33d77a0-0d3a-4422-b91c-7cb052a21aaa",
      "appeal_id": "a82d77a0-0d3a-4422-b91c-7cb052a21def",
      "sender_role": "MERCHANT",
      "body": "Предоставляю выписку из банка.",
      "file_ids": [],
      "created_at": "2026-07-12T17:27:00Z"
    },
    {
      "message_id": "b33d77a0-0d3a-4422-b91c-7cb052a21bbb",
      "appeal_id": "a82d77a0-0d3a-4422-b91c-7cb052a21def",
      "sender_role": "ADMIN",
      "body": "Доказательство получено. Начинаем проверку.",
      "file_ids": [],
      "created_at": "2026-07-12T17:27:05Z"
    }
  ],
  "next_page_token": ""
}

On this page