S
docs.syncra.money
Платежи

Прием платежей (PayIn)

Интеграция приема P2P-платежей с помощью Syncra Merchant API V2

Прием платежей (PayIn)

Функционал входящих платежей (PayIn) позволяет мерчантам автоматизировать приём средств от физических лиц через СБП, банковские карты (Card2Card), международные переводы по IBAN и другие методы онлайн-оплаты. Платформа не привязана к конкретной стране: доступные рынки (РФ, СНГ, Турция, ЕС, ...) определяются конфигурацией вашего тенанта, а выбор канала под капотом payment_method_id — типом реквизита (RequisiteType). Полный справочник типов — в разделе Типы реквизитов.


Процесс оплаты (PayIn Flow)

Загрузка диаграммы...
  1. Создание сделки. Мерчант отправляет POST /deals/payin, указывая amount, currency, idempotency_key и payment_method_id (UUID или слаг метода из каталога).
  2. Получение URL оплаты. В ответе — deal_id (UUID сделки) и payment_url (hosted payform).
  3. Перевод средств. Клиент переходит по payment_url, видит реквизиты P2P-трейдера и совершает перевод в мобильном банке.
  4. Уведомление. После верификации поступления шлюз шлёт мерчанту webhook deal.completed (см. Callbacks).

Эндпоинт создания PayIn

POST /api/v1/p2p/merchant/deals/payin

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

ПолеТипОбяз.Описание
amountint64ДаСумма в минорных единицах (копейки для RUB). 150000 = 1500.00 RUB.
currencystringДаISO 4217 код валюты ("RUB", "KZT", "TRY", ...).
idempotency_keystringДаУникальный ID запроса на стороне мерчанта (идемпотентность).
client_idstringНетID плательщика в вашей системе (ваш сервис, ЛК).
payment_method_idstringДа — REQUIRED; пусто → 400 INVALID_ARGUMENTСсылка на метод оплаты: UUID (канонический формат, быстрый путь без запроса к БД) или слаг (sbp_rub, card_rub, ...) из GET /api/v1/p2p/merchant/payment-methods — оба значения резолвятся в один и тот же метод (см. Banks). Неизвестное значение → 400 со списком допустимых слагов. Каскад маршрутизации методо-скопирован, поэтому метод обязателен.
currency_idstring(UUID)НетUUID валюты. Обязателен для pass-matching; для обычной сделки можно опустить.
target_bankstringНетКанонический код банка реквизита (bank-routing): значение bank_name из GET /banks?method=.... Кнопка «1click Альфа» = метод + target_bank: alfa — каскад выдаёт реквизит только этого банка (strict-политика тенанта). Не передан — маршрутизация по методу как раньше. Неизвестный код → 400 с перечнем допустимых.
payer_bankstringНетПодсказка по банку ПЛАТЕЛЬЩИКА (free-text, отображение; не маршрутизирует — для маршрутизации есть target_bank).
descriptionstringНетОписание платежа от мерчанта. Выводится клиенту на платёжной странице.
callback_urlstringНетURL обработчика вебхуков для этой сделки (переопределяет адрес доставки webhook_url мерчанта; только PayIn).
is_client_commissionboolНетРежим комиссии: true — платит клиент; false (по умолчанию) — удерживается из суммы мерчанта.
modestringНетРежим презентации сделки: "HOSTED" (по умолчанию; то же, что пусто) — реквизиты на hosted payform, или "DIRECT" — реквизиты получателя возвращаются в ответе API (см. Режим Direct (H2H)). Матчинг, эскроу и вебхуки не зависят от режима. Любое другое значение → 400 INVALID_ARGUMENT («invalid mode ...: must be HOSTED, DIRECT or empty (default HOSTED)»); сделка не создаётся.

callback_url переопределяет только адрес доставки вебхука. Он НЕ снимает предусловие настройки webhook_url + webhook secret на уровне аккаунта мерча (кабинет → Вебхуки). Без аккаунт-level настройки создание сделки → 400 FAILED_PRECONDITION.

URL-адреса редиректа клиента (success_url, failed_url) не являются полями proto CreateMerchantPayInRequest и не передаются в запросе. Они настраиваются на уровне мерчанта через admin panel и применяются ко всем сделкам автоматически. Проброс per-deal success_url/failed_url в V2 не предусмотрен.

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

{
  "amount": 250050,
  "currency": "RUB",
  "idempotency_key": "order_uuid_abc123456",
  "client_id": "player_user_9921",
  "payment_method_id": "550e8400-e29b-41d4-a716-446655440000",
  "currency_id": "9a3f1c2e-1234-4abc-9def-56789abcdef0",
  "payer_bank": "sber",
  "description": "Пополнение счёта клиента player_user_9921",
  "callback_url": "https://merchant.example/callbacks/syncra",
  "is_client_commission": false
}

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

Ответ — объект MerchantDealInfo в поле deal верхнего уровня (обёртки data нет). Ключевое поле идентификатора — deal_id (НЕ id). URL платёжной страницы — payment_url (НЕ payment_page_url).

{
  "deal": {
    "deal_id": "7ac148fe-19a3-45bb-b992-019fac55b721",
    "status": "ESCROW_LOCKED",
    "amount": "250050",
    "currency": "RUB",
    "created_at": "2026-07-12T17:20:00Z",
    "client_id": "player_user_9921",
    "deal_type": "PAYIN",
    "updated_at": "2026-07-12T17:20:00Z",
    "payment_url": "https://checkout.syncra.money/pay/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
    "target_requisite": "",
    "received_amount": "0",
    "amount_match_status": "",
    "amount_usdt": "512400",
    "exchange_rate_micro": "488181",
    "mode": "",
    "requisites": null
  }
}

payment_url строится как {checkout_base_url}/pay/{order.Hash}, где order.Hash — 64-hex public-идентификатор сделки (unguessable, без auth). target_requisite пуст для PayIn (это поле payout-сделок).

В ответе 200 OK статус — ESCROW_LOCKED: подбор трейдера и эскроу-лок выполняются синхронно в рамках запроса (при неудачном матчинге вызов завершается ошибкой, а не возвращает «висящую» сделку). INITIALIZED — кратковременный транзитный статус «сделка создана, реквизиты ещё не выделены»: в ответе создания он практически не наблюдается, но возможен в GET /deals/GET /deals/{id} до завершения подбора.

Пример ответа для TRY (BANK_TRANSFER / Havale)

Создание PayIn в TRY ничем не отличается от RUB: тот же эндпоинт, тот же набор полей — меняются currency и payment_method_id (UUID IBAN-метода из GET /payment-methods).

{
  "deal": {
    "deal_id": "9d4a07c2-3b1e-4f6a-9c25-8e7f1b0d3a44",
    "status": "ESCROW_LOCKED",
    "amount": "2500000",
    "currency": "TRY",
    "created_at": "2026-08-18T10:05:00Z",
    "client_id": "player_user_4477",
    "deal_type": "PAYIN",
    "updated_at": "2026-08-18T10:05:00Z",
    "payment_url": "https://checkout.syncra.money/pay/f3e2d1c0b9a80796f5e4d3c2b1a0f9e8d7c6b5a49382716055f4e3d2c1b0a9988",
    "target_requisite": "",
    "received_amount": "0",
    "amount_match_status": "",
    "amount_usdt": "512400",
    "exchange_rate_micro": "205049",
    "mode": "",
    "requisites": null
  }
}

amount: 2500000 = 25 000.00 TRY (минорные единицы — куруш, 2 знака). target_requisite пуст всегда для PayIn — это поле payout-сделок; реквизиты получателя (IBAN, держатель, банк) клиент получает на платёжной странице (см. ниже).


Режим Direct (H2H): реквизиты в ответе

POST /deals/payin поддерживает два режима презентации сделки: необязательное поле mode определяет, кто отрисовывает реквизиты получателя. Матчинг, эскроу, комиссии, статусная модель и вебхуки одинаковы в обоих режимах.

HOSTED (по умолчанию)DIRECT
Реквизиты видитклиент на нашей payformмерчант в ответе API
Экран оплатыhosted checkout по payment_urlваш интерфейс (ЛК мерча)
Подходит, еслинужна скорость и готовая страницаплатёжный экран должен остаться у вас

В режиме DIRECT наша платёжная страница клиенту не показывается: тот же эндпоинт дополнительно возвращает блок requisites (реквизиты, держатель, банк, дедлайн), который вы отрисовываете в своём интерфейсе. payment_url возвращается и для DIRECT — суффикс {hash} остаётся capability клиента и нужен для подтверждения оплаты (см. ниже).

Флоу мерчанта (Direct Flow)

  1. Создание сделки. POST /deals/payin с "mode": "DIRECT" — набор остальных полей идентичен HOSTED.
  2. Отрисовка. Покажите клиенту сумму (amount), курс (exchange_rate_micro, amount_usdt) и реквизиты из requisites; заведите таймер до requisites.expires_at (равен payment_timeout_at сделки).
  3. Оплата. Клиент переводит средства в своём банке по реквизитам — канал, каскад и трейдер те же, что на hosted-странице.
  4. Подтверждение оплаты. Ваш сервер (или кнопка «Я оплатил» в вашем UI) вызывает notify-payment — статус сделки → PAYMENT_NOTIFIED.
  5. Верификация и завершение. Трейдер подтверждает поступление, сделка доводится до COMPLETED штатным циклом verify → complete — от вас действий не требуется.
  6. Вебхуки. Приходит стандартный набор deal.* (см. События вебхуков); тела вебхуков PII не несут.

Инварианты режима

  • Одноразовость вместе со сделкой. Реквизит выдан матчингом каскада именно этой сделке и действителен до requisites.expires_at (== payment_timeout_at). После истечения сделка переходит в терминальный EXPIRED (терминальные статусы неизменяемы), реквизит больше не действует. Перевыдача = новая сделка с новым idempotency_key — повтор со старым ключом вернёт ту же EXPIRED-сделку; отдельной ручки перевыдачи нет.
  • Единственный источник. requisites.requisite_id — это ровно тот реквизит, который каскадный матчинг привязал к сделке; сохраните его в своих журналах для сверки. Реквизиты в рамках живой сделки не регенерируются и не меняются.
  • PII-видимость. Блок requisites возвращается только пока сделка ждёт оплаты (ESCROW_LOCKED, PAYMENT_NOTIFIED); в терминальных статусах — requisites: null, PAN/IBAN повторно не раскрываются.
  • Ownership. Реквизиты доступны только мерчанту-владельцу сделки: GET /deals/{deal_id} по чужой DIRECT-сделке → 403 PERMISSION_DENIED, блока реквизитов в ответе нет.
  • Курс без изменений. amount, amount_usdt и exchange_rate_micro приходят как раньше (int64-строками) — показывайте курс клиенту тем же кодом, что и для HOSTED.

Ответ DIRECT-сделки (mode + requisites)

Помимо стандартных полей MerchantDealInfo, ответ DIRECT-сделки несёт mode: "DIRECT" и сообщение requisites (у HOSTED-сделок — mode: "" и requisites: null). Поля requisites:

ПолеТипОписание
requisite_typestringТип реквизита — то же имя и тот же словарь, что в requisite_types каталога GET /payment-methods: BANK_CARD — банковская карта, E_WALLET — электронный кошелёк, SBP — СБП (перевод по номеру телефона), SIM — мобильная коммерция (перевод на номер оператора связи), BANK_TRANSFER — международный банковский перевод. Это тип реквизита, а не платёжный метод: метод оплаты по-прежнему задаётся полем payment_method_id при создании сделки (например card_rub, sbp_rub).
card_numberstringПолный PAN (без маскировки). Только requisite_type=BANK_CARD; у остальных — "".
cardholder_namestringФИО держателя карты/счёта. Заполняется у BANK_CARD/BANK_TRANSFER; у остальных типов — если реквизит содержит имя держателя.
bank_namestringОтображаемое имя банка/канала (из каталога методов).
phonestringНомер телефона для перевода. Заполняется для E_WALLET/SBP/SIM.
ibanstringПолный IBAN. Только requisite_type=BANK_TRANSFER; у остальных — "".
qr_payloadstringЗарезервировано под QR; сегодня всегда "".
requisite_idstring(UUID)Идентификатор выданного реквизита (для сверки).
expires_atstring(RFC3339)Дедлайн оплаты; равен payment_timeout_at сделки.

Пример ответа (200 OK, mode: DIRECT)

{
  "deal": {
    "deal_id": "7ac148fe-19a3-45bb-b992-019fac55b721",
    "status": "ESCROW_LOCKED",
    "amount": "250050",
    "currency": "RUB",
    "created_at": "2026-09-11T10:00:00Z",
    "client_id": "player_user_9921",
    "deal_type": "PAYIN",
    "updated_at": "2026-09-11T10:00:00Z",
    "payment_url": "https://checkout.syncra.money/pay/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
    "target_requisite": "",
    "received_amount": "0",
    "amount_match_status": "",
    "amount_usdt": "500100",
    "exchange_rate_micro": "50000000",
    "mode": "DIRECT",
    "requisites": {
      "requisite_type": "BANK_CARD",
      "card_number": "2202201234567890",
      "cardholder_name": "Иван Иванович",
      "bank_name": "Sberbank",
      "phone": "",
      "iban": "",
      "qr_payload": "",
      "requisite_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "expires_at": "2026-09-11T10:20:00Z"
    }
  }
}

amount_usdt — USDT-эквивалент сделки в минорных единицах USDT (500100 = 5001.00 USDT), exchange_rate_micro — эффективный курс на момент создания (50000000 = 50.00 RUB за 1 USDT): то же поле курса, что и у HOSTED-сделок.

Компактные блоки requisites остальных видов:

SBP — СБП, перевод по номеру телефона:

{
  "requisite_type": "SBP",
  "card_number": "",
  "cardholder_name": "",
  "bank_name": "T-Bank",
  "phone": "+79001234567",
  "iban": "",
  "qr_payload": "",
  "requisite_id": "6f1d2b3c-9e47-4a58-8c21-0b5d3e7f9a11",
  "expires_at": "2026-09-11T10:20:00Z"
}

SIM — мобильная коммерция, перевод на номер оператора связи (для e-wallet-реквизитов — тот же формат блока с requisite_type=E_WALLET):

{
  "requisite_type": "SIM",
  "card_number": "",
  "cardholder_name": "",
  "bank_name": "Beeline",
  "phone": "+79067654321",
  "iban": "",
  "qr_payload": "",
  "requisite_id": "8c2e4f60-1d3b-4c5a-9e77-2f8a0b1c3d52",
  "expires_at": "2026-09-11T10:20:00Z"
}

BANK_TRANSFER — международный перевод (Havale / IBAN):

{
  "requisite_type": "BANK_TRANSFER",
  "card_number": "",
  "cardholder_name": "Ahmet Yilmaz",
  "bank_name": "Ziraat — Havale",
  "phone": "",
  "iban": "TR330006100519786457841326",
  "qr_payload": "",
  "requisite_id": "2b7a9c10-5e63-4f88-a2d4-6c0b7e9f1a33",
  "expires_at": "2026-09-11T10:20:00Z"
}

Если матчинг не готов сразу (SLU)

Mainline-матчинг синхронен: обычный ответ создания — уже ESCROW_LOCKED с готовыми реквизитами. Прочие исходы:

Ответ созданияЧто делать мерчу
200, status: "ESCROW_LOCKED", requisites заполненыШтатный путь: рисуйте реквизиты и таймер.
200, status: "INITIALIZED", requisites: nullСделка ждёт вердикта матчинга: дождитесь вебхука deal.escrow_locked, затем заберите реквизиты через GET /api/v1/p2p/merchant/deals/{deal_id} (тот же блок requisites).
503 UNAVAILABLE («no matching trader available, retry later»)Свободного провайдера нет; сделка фиксируется сразу в терминальном EXPIRED. Создайте платёж заново позже — с новым idempotency_key (повтор со старым вернёт EXPIRED-сделку). Ретрай с backoff — по Retry-матрице.

Подтверждение оплаты клиентом (notify-payment)

Две публичные ручки выполняют один и тот же переход ESCROW_LOCKED → PAYMENT_NOTIFIED; выберите одну.

Ручка по checkout-хэшу (рекомендуемая)

POST /api/v1/p2p/checkout/{hash}/notify-payment

Авторизация не нужна: {hash} — 64-hex суффикс payment_url после /pay/ (для DIRECT payment_url возвращается так же, как для HOSTED). Тело необязательно; можно передать UUID файлов-подтверждений (скриншоты чеков):

# hash = последний сегмент payment_url после /pay/
curl -X POST https://api.syncra.money/api/v1/p2p/checkout/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/notify-payment \
  -H "Content-Type: application/json" \
  -d '{"payment_proof_file_ids": ["f3a1b2c3-4d5e-6f7a-8b9c-0d1e2f3a4b5c"]}'
КодКогда
200Сделка → PAYMENT_NOTIFIED; ответ — обновлённый CheckoutView. Повторный вызов с тем же hash идемпотентно коллапсирует в уже выполненный переход (детерминированный серверный ключ): повторные нажатия «Я оплатил» не создают вторых переходов.
400Тело — невалидный JSON.
403Клиент заблокирован (антифрод).
404Неизвестный hash.
409Недопустимый переход: сделка уже PAYMENT_NOTIFIED или в более позднем/терминальном статусе — второй сигнал «Я оплатил» не принимается.

Ручка по order_id (альтернатива)

POST /api/v1/p2p/orders/{order_id}/notify-payment

Тоже публичная (у плательщика нет аккаунта) — capability-гейтом служит обязательный deal_hash. Тело:

ПолеТипОбяз.Описание
order_idstring(UUID)ДаИдентификатор сделки (в пути URL).
deal_hashstringДа64-hex суффикс payment_url — capability плательщика; пустой или несовпадающий → 403 PERMISSION_DENIED.
idempotency_keystring(UUID)ДаКлюч уведомления: повтор с тем же ключом → 200 replay; новый ключ по сделке, уже получившей сигнал (PAYMENT_NOTIFIED) → 400 FAILED_PRECONDITION — не ретраить.
tenant_idstring(UUID)НетРезолвируется из самой сделки по order_id.
file_idsarrayНетUUID файлов-подтверждений из files-сервиса.
curl -X POST https://api.syncra.money/api/v1/p2p/orders/7ac148fe-19a3-45bb-b992-019fac55b721/notify-payment \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
    "idempotency_key": "0d3ee6e2-7c4f-4d3a-9f2e-6b8a1c5d9e01",
    "deal_hash": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
  }'

После PAYMENT_NOTIFIED дальше — штатный цикл: трейдер verify → complete и вебхуки deal.payment_verified, deal.completed (см. События вебхуков).

Режим по умолчанию — HOSTED: поле mode необязательно, существующие интеграции не меняются. У хостед-сделок ответ несёт пустой mode ("") и requisites: null — прежние значения сохранены, в JSON добавились только два новых ключа (сериализатор эмитит все proto-поля). Обработка ошибок и Retry-матрица не меняются: 400 не ретраить, 503 — ретраить с backoff.


Статусы PayIn сделки

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

Статус V2Финальный?Описание
INITIALIZEDНетСделка создана, ожидает эскроу-лока.
ESCROW_LOCKEDНетЭскроу зарезервирован, реквизиты выданы.
PAYMENT_NOTIFIEDНетКлиент нажал «Я оплатил».
PAYMENT_VERIFIEDНетПоступление подтверждено, сумма сверена.
COMPLETEDДаУспешно завершена (amount_match_status=full/partial/overpaid).
EXPIREDДаИстекла по payment_timeout_at.
APPEALEDНетОткрыта апелляция.
REFUND_PENDINGНетВнутренний транзит к CANCELLED: отклонённая апелляция или third-party-флаг. Внешне — пара вебхуков deal.refund_pending → deal.cancelled; действий мерчанта не требует.
SPAM_REJECTEDДаОтклонена анти-spam.
SOFT_DISPUTEDНетМягкий спор.
CANCELLEDДаКлиент или мерч явно отменил сделку до подтверждения платежа, либо финализирован REFUND_PENDING-транзит (см. Отмена сделки).

Отмена сделки (Cancel)

PayIn-сделку можно явно отменить до подтверждения платежа — статус переходит в терминальный CANCELLED. Отмена доступна из INITIALIZED, ESCROW_LOCKED и PAYMENT_NOTIFIED; после PAYMENT_VERIFIED (и тем более из COMPLETED/APPEALED и прочих состояний) отмена невозможна — платёж уже подтверждён трейдером.

Два эндпоинта, один и тот же переход FSM:

Отмена мерчантом

POST /api/v1/p2p/merchant/deals/{deal_id}/cancel

Аутентификация — стандартная HMAC-пара мерчанта (X-Merchant-Token / X-Signature, как у остальных /merchant/*-эндпоинтов). Тело:

ПолеТипОбяз.Описание
reasonstringНетПроизвольная причина отмены (логируется платформой).

Ответ 200 OK — объект MerchantDealInfo отменённой сделки в поле deal (та же структура, что у GET /deals/{deal_id}, со status: "CANCELLED").

Отмена клиентом (платёжная страница)

POST /api/v1/p2p/checkout/{hash}/cancel

Публичный эндпоинт по checkout-хэшу из payment_url (авторизация не нужна, тело пустое) — вызывается кнопкой отмены на hosted payform. Возвращает обновлённый CheckoutView.

Правила и коды ответов

КодКогда
200Сделка отменена; повторный вызов на уже-CANCELLED сделке идемпотентно возвращает 200 без повторного перехода.
400Недопустимый переход — сделка уже в PAYMENT_VERIFIED/COMPLETED/терминальном статусе.
404deal_id не найден, чужой, или это ID PayOut-выплаты (cancel — операция только PayIn-контура: PayOut-сделки ищутся в отдельной таблице и отвечают 404, не раскрывая существование). Для checkout-ветки — неизвестный hash.
409Checkout-ветка: сделка в терминальном статусе, отменить уже нельзя.
  • Отмена после ESCROW_LOCKED автоматически снимает эскроу-холды (order-level + per-match) — деньги «размораживаются» без участия мерчанта.
  • fee_breakdown обнуляется, матчи сделки переходят в FAILED (та же fail-family семантика, что у EXPIRED).
  • О факте отмены приходит webhook deal.cancelled (см. Callbacks).

Расхождения сумм (full / partial / overpaid)

Фактическая сумма, переведённая плательщиком, сверяется с суммой сделки: результат фиксируется в полях received_amount (минорные единицы) и amount_match_status сделки и передаётся в вебхуках после верификации.

Допуск равен нулю. Автоматическое завершение возможно только при точном совпадении сумм — до копейки, без округлений, комиссионных или курсовых окон. Любое несовпадение (partial/overpaid) — платёж считается несовпавшим: проводки по избытку или недостаче не возникают, сделка живёт до таймаута и завершается терминальной стадией; далее — только ручное рассмотрение через апелляцию. Автозавершение «по фактической сумме» не существует и не может быть включено.

amount_match_statusСитуацияЧто происходит
fullПолучено ровно столько, сколько заявлено (точное совпадение).Стандартный флоу: verify → complete, вебхук deal.completed.
partialПолучено меньше заявленного.Платёж не совпал с суммой сделки: автозавершение блокируется, сделка остаётся в текущем статусе и передаётся операторам. Таймаут продолжает тикать: если оператор не изменил сумму сделки (см. Изменение суммы) и не зачёл платёж, сделка истечёт в EXPIRED с зафиксированным received_amount — далее возможен зачёт по факту.
overpaidПолучено больше заявленного.Платёж не совпал с суммой сделки: автозавершение блокируется так же; избыток — посторонний приход, по нему не возникает никаких проводок: платформа фиксирует сведения о платеже (сумма, время, источник, связанная сделка) и передаёт их операторам; дальнейшее разбирательство — вручную через апелляцию.

Изменение суммы сделки (amend)

Пока сделка живая (статусы INITIALIZED, ESCROW_LOCKED, PAYMENT_NOTIFIED, SOFT_DISPUTED — всё до PAYMENT_VERIFIED), оператор платформы может изменить её фиатовую сумму: при частичной оплате, ошибке мерчанта в сумме и т.п. Это операторское действие кабинета (не merchant-API):

POST /api/v1/p2p/deals/{deal_id}/amount
Поле телаТипОбяз.Описание
amount_fiatint64ДаНовая сумма в минорных единицах (> 0).
reasonstringДаПричина изменения — фиксируется в неизменяемом аудите дословно.
idempotency_keystring(UUID)ДаКлюч действия оператора: повтор с тем же ключом и той же суммой → replay; с другой суммой → 409.
expected_amount_versionint64НетОптимистичная блокировка (механизм вида If-Match): версия суммы, которую видел оператор. 0/не задано — не проверять; несовпадение → 409, оператор перечитывает сделку.

Что происходит при amend (атомарно, одной транзакцией):

  • amount_usdt пересчитывается по курсу самой сделки (exchange_rate_micro, зафиксирован при создании — условия акта не меняются);
  • эскроу-холды пересоздаются под новую сумму: order-level + все per-match (идемпотентные ключи холдов производны от (deal_id, amount_version));
  • amount_version увеличивается на 1 (если передан expected_amount_version и он не совпал — отказ 409);
  • пишется неизменяемая (INSERT-only) аудиторская строка p2p_deal_amount_changes и эмитится событие deal.amount_changed (только V2).

После PAYMENT_VERIFIED изменение суммы невозможно — сумма уже авторизована; расхождения закрываются сеттлментом (ниже) или возвратом.


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

Сделка в EXPIRED не оживает никогда (терминальные статусы неизменяемы). Если к моменту истечения платёж фактически пришёл (received_amount > 0, обычно amount_match_status = partial/overpaid), оператор закрывает спор зачётом: по резолву апелляции создаётся новая сделка-сеттлмент:

POST /api/v1/p2p/appeals/{appeal_id}/settlement
Поле телаТипОбяз.Описание
received_amount_overrideint64НетПереопределение суммы (по умолчанию — received_amount оригинала). При задании обязательна reason (аудит-флаг override_used).
reasonstringПри overrideОбоснование ручного переопределения.
idempotency_keystring(UUID)ДаКлюч действия; повтор — replay существующего сеттлмента.

Свойства сеттлмента:

  • это новый deal_id со связями parent_deal_id (истёкший оригинал) и origin_appeal_id (одна апелляция → максимум один сеттлмент, UNIQUE);
  • сумма = received_amount оригинала; курс — новый, на момент резолва (фиксируется в записи сеттлмента);
  • путь денег стандартный: эскроу-холд → verify → complete одним действием, комиссия/леджер считаются как у обычной сделки;
  • по сеттлменту приходит полный набор вебхуков deal.created … deal.completed (обе версии API);
  • апелляция резолвится SATISFIED с ссылкой на сеттлмент в resolution_note;
  • оригинал остаётся EXPIRED навсегда, его received_amount сохраняется как исторический факт. Листинги показывают обе записи со связью (parent_deal_id).

Требования: родитель EXPIRED, апелляция в OPEN/IN_PROGRESS/REOPENED, received_amount > 0 (или обоснованный override). Нарушения → 409/400.


Поддерживаемые методы оплаты

Единый контракт для всех методов. Эндпоинт POST /deals/payin принимает один и тот же набор полей для любого метода оплаты — метод-специфичных параметров в запросе не существует. Выбор метода — это только payment_method_id (UUID); вся специфика канала (тип реквизита, формат отображения, инструкция для плательщика) инкапсулирована на стороне платформы и показывается на платёжной странице. Если при подключении нового метода от вас не требуется ничего, кроме получения его UUID, — это норма, а не исключение.

Не зашивайте UUID методов в код. Идентификаторы генерируются для каждого тенанта отдельно и меняются при пересоздании метода; набор доступных методов и их status управляются платформой (метод может быть включён, отключён, заменён) без релиза на вашей стороне. Читайте GET /api/v1/p2p/merchant/payment-methods при инициализации интеграции и обновляйте закешированный список при каждом старте платёжной сессии: тогда новые методы появляются у ваших клиентов без изменений вашего кода, а отключённые корректно пропадают из кассы. Храните привязку ваших товаров/категорий к payment_method_id в своей базе, а не в конфиге.

Доступные методы определяются конфигурацией тенанта (таблица p2p_payment_methods). При создании платежа передайте ссылку на метод в поле payment_method_id: UUID (канонический формат — поле id каталога) или слаг (поле slug). Канонический источник обеих форм — мерчантский эндпоинт GET /api/v1/p2p/merchant/payment-methods: он возвращает методы вашего тенанта с их id (UUID), slug, name, requisite_types, currency_code и status (см. Banks → Получение списка методов через API). Неизвестный UUID/слаг отклоняется с 400 INVALID_ARGUMENT, в тексте ошибки перечислены допустимые слаги активных методов.

GET /banks не возвращает UUID методов — его поле methods содержит типы реквизитов (RequisiteType: BANK_CARD, SBP, BANK_TRANSFER, ...), а не идентификаторы. Использовать значения из GET /banks в payment_method_id нельзя — берите UUID (id) или слаг (slug) только из GET /payment-methods.

Типичные каналы (в payment_method_id передавайте id (UUID) или slug конкретного метода вашего тенанта из GET /payment-methods — канонические слаги каталога, например sbp_rub, card_rub, mobcom_rub, express-havale):

  • СБП — перевод по телефону. RequisiteType: SBP (слаг каталога — sbp_rub).
  • Перевод по номеру карты. RequisiteType: BANK_CARD (слаг — card_rub).
  • Быстрая оплата Сбербанк/Т-Банк. RequisiteType: BANK_CARD / SBP.
  • Методы мобильной коммерции — пополнение счёта мобильного оператора (RequisiteType: SIM; слаг — mobcom_rub). Реквизит для плательщика — номер телефона оператора связи; название оператора отображается на платёжной странице как «банк получателя». Номер телефона самого плательщика (MSISDN) для оплаты не требуется и никуда не передаётся: перевод выполняется плательщиком из своего мобильного приложения банка.
  • Международный перевод по IBAN (ISO 13616) для не-RU рынков (TRY, EUR, ...). RequisiteType: BANK_TRANSFER (слаг вида express-havale — задаётся при заведении метода для вашего тенанта).

BANK_TRANSFER — это не отдельный эндпоинт, а один из типов реквизита. PayIn создаётся тем же POST /deals/payin с payment_method_id соответствующего IBAN-метода; специфика отображается только на платёжной странице (CheckoutView.iban).


Где реквизиты получателя при PayIn

В режиме по умолчанию (HOSTED) реквизиты получателя (номер карты, телефон СБП, IBAN для BANK_TRANSFER) не возвращаются в теле ответа POST /deals/payin — там есть только deal_id и payment_url (PII-минимизация: платёжные данные не покидают платёжный контур). Единственное исключение — режим Direct (H2H): у DIRECT-сделок блок requisites отдаётся мерчанту-владельцу прямо в ответе создания и в GET /deals/{deal_id}.

Клиент получает реквизиты, открывая payment_url: платёжная страница дёргает публичный эндпоинт

GET /api/v1/p2p/checkout/{hash}

где {hash} — 64-hex идентификатор из payment_url (capability-токен, авторизация не нужна). В ответе (CheckoutView):

ПолеЧто содержит
ibanIBAN получателя (ISO 13616) для BANK_TRANSFER-сделок.
holder_nameФИО держателя карты/счёта трейдера.
payment_method_nameНазвание банка/канала (например, «Havale — Ziraat»).
card_number / phone_numberПолный PAN / телефон для BANK_CARD / SBP / SIM (для SIM-методов — номер телефона оператора связи).

Полный контракт CheckoutView и пример ответа — в SDK Examples §9; экран IBAN-оплаты описан в Hosted Checkout → Перевод по IBAN. В терминальных состояниях (COMPLETED, EXPIRED, ...) чувствительные поля обнуляются.


Пример на curl

curl -X POST https://api.syncra.money/api/v1/p2p/merchant/deals/payin \
  -H "X-Merchant-Token: your_token_here" \
  -H "X-Timestamp: 1783856120" \
  -H "X-Signature: t=1783856120,v1=9e87d0c3c8801d0a5198d023b6b19a16f2c8d2037920ab3e09819cd8e412cfab" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 100000,
    "currency": "RUB",
    "payment_method_id": "550e8400-e29b-41d4-a716-446655440000",
    "idempotency_key": "my_unique_p2p_key_001"
  }'

События вебхуков

При создании PayIn генерируются следующие события (отправляются на ваш callback URL):

СобытиеСтатус заказаОписание
deal.createdINITIALIZEDЗаказ создан
deal.escrow_lockedESCROW_LOCKEDЗалог USDT зарезервирован
deal.payment_notifiedPAYMENT_NOTIFIEDКлиент нажал «Я оплатил»
deal.amount_changed(без смены статуса)Оператор изменил сумму живой сделки; payload: old_amount + новая amount + amount_version. Только V2
deal.payment_verifiedPAYMENT_VERIFIEDТрейдер подтвердил получение
deal.completedCOMPLETEDСделка завершена, средства зачислены
deal.expiredEXPIREDЗаказ истёк по таймауту
deal.appealedAPPEALEDОткрыт диспут
deal.refund_pendingREFUND_PENDINGТранзит-уведомление: сделка финализируется отменой; за ним всегда следует deal.cancelled
deal.cancelledCANCELLEDКлиент или мерч явно отменили сделку до подтверждения платежа

На этой странице