S
docs.syncra.money
API ReferenceMerchant API

Прием платежей (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; для обычной сделки можно опустить.
payer_bankstringНетПодсказка по банку плательщика для cascade routing (приоритизирует реквизиты того же банка).
descriptionstringНетОписание платежа от мерчанта. Выводится клиенту на платёжной странице.
callback_urlstringНетURL обработчика вебхуков для этой сделки (переопределяет адрес доставки webhook_url мерчанта; только PayIn).
is_client_commissionboolНетРежим комиссии: true — платит клиент; false (по умолчанию) — удерживается из суммы мерчанта.

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": ""
  }
}

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


Статусы 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НетИнициирован возврат.
SPAM_REJECTEDДаОтклонена анти-spam.
SOFT_DISPUTEDНетМягкий спор.
CANCELLEDДаИгрок или мерч явно отменил сделку до подтверждения платежа (см. Отмена сделки).

Отмена сделки (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 сделки и передаётся в вебхуках после верификации.

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

Изменение суммы сделки (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НетОптимистичная блокировка (Stripe 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.createddeal.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

Реквизиты получателя (номер карты, телефон СБП, IBAN для BANK_TRANSFER) никогда не возвращаются в теле ответа POST /deals/payin — там есть только deal_id и payment_url (PII-минимизация: платёжные данные не покидают платёжный контур).

Клиент получает реквизиты, открывая 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.cancelledCANCELLEDИгрок или мерч явно отменили сделку до подтверждения платежа

On this page