Прием платежей (PayIn)
Интеграция приема P2P-платежей с помощью Syncra Merchant API V2
Прием платежей (PayIn)
Функционал входящих платежей (PayIn) позволяет мерчантам автоматизировать
приём средств от физических лиц через СБП, банковские карты (Card2Card),
международные переводы по IBAN и другие методы онлайн-оплаты. Платформа не
привязана к конкретной стране: доступные рынки (РФ, СНГ, Турция, ЕС, ...)
определяются конфигурацией вашего тенанта, а выбор канала под капотом
payment_method_id — типом реквизита (RequisiteType). Полный справочник
типов — в разделе Типы реквизитов.
Процесс оплаты (PayIn Flow)
- Создание сделки. Мерчант отправляет
POST /deals/payin, указываяamount,currency,idempotency_keyиpayment_method_id(UUID или слаг метода из каталога). - Получение URL оплаты. В ответе —
deal_id(UUID сделки) иpayment_url(hosted payform). - Перевод средств. Клиент переходит по
payment_url, видит реквизиты P2P-трейдера и совершает перевод в мобильном банке. - Уведомление. После верификации поступления шлюз шлёт мерчанту webhook
deal.completed(см. Callbacks).
Эндпоинт создания PayIn
POST /api/v1/p2p/merchant/deals/payinПараметры запроса (JSON Body)
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
amount | int64 | Да | Сумма в минорных единицах (копейки для RUB). 150000 = 1500.00 RUB. |
currency | string | Да | ISO 4217 код валюты ("RUB", "KZT", "TRY", ...). |
idempotency_key | string | Да | Уникальный ID запроса на стороне мерчанта (идемпотентность). |
client_id | string | Нет | ID плательщика в вашей системе (казино, ЛК). |
payment_method_id | string | Да — REQUIRED; пусто → 400 INVALID_ARGUMENT | Ссылка на метод оплаты: UUID (канонический формат, быстрый путь без запроса к БД) или слаг (sbp_rub, card_rub, ...) из GET /api/v1/p2p/merchant/payment-methods — оба значения резолвятся в один и тот же метод (см. Banks). Неизвестное значение → 400 со списком допустимых слагов. Каскад маршрутизации методо-скопирован, поэтому метод обязателен. |
currency_id | string(UUID) | Нет | UUID валюты. Обязателен для pass-matching; для обычной сделки можно опустить. |
payer_bank | string | Нет | Подсказка по банку плательщика для cascade routing (приоритизирует реквизиты того же банка). |
description | string | Нет | Описание платежа от мерчанта. Выводится клиенту на платёжной странице. |
callback_url | string | Нет | URL обработчика вебхуков для этой сделки (переопределяет адрес доставки webhook_url мерчанта; только PayIn). |
is_client_commission | bool | Нет | Режим комиссии: 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/*-эндпоинтов). Тело:
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
reason | string | Нет | Произвольная причина отмены (логируется платформой). |
Ответ 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/терминальном статусе. |
404 | deal_id не найден, чужой, или это ID PayOut-выплаты (cancel — операция только PayIn-контура: PayOut-сделки ищутся в отдельной таблице и отвечают 404, не раскрывая существование). Для checkout-ветки — неизвестный hash. |
409 | Checkout-ветка: сделка в терминальном статусе, отменить уже нельзя. |
- Отмена после
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_fiat | int64 | Да | Новая сумма в минорных единицах (> 0). |
reason | string | Да | Причина изменения — фиксируется в неизменяемом аудите дословно. |
idempotency_key | string(UUID) | Да | Ключ действия оператора: повтор с тем же ключом и той же суммой → replay; с другой суммой → 409. |
expected_amount_version | int64 | Нет | Оптимистичная блокировка (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_override | int64 | Нет | Переопределение суммы (по умолчанию — received_amount оригинала). При задании обязательна reason (аудит-флаг override_used). |
reason | string | При override | Обоснование ручного переопределения. |
idempotency_key | string(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
Реквизиты получателя (номер карты, телефон СБП, 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):
| Поле | Что содержит |
|---|---|
iban | IBAN получателя (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.created | INITIALIZED | Заказ создан |
deal.escrow_locked | ESCROW_LOCKED | Залог USDT зарезервирован |
deal.payment_notified | PAYMENT_NOTIFIED | Клиент нажал «Я оплатил» |
deal.amount_changed | (без смены статуса) | Оператор изменил сумму живой сделки; payload: old_amount + новая amount + amount_version. Только V2 |
deal.payment_verified | PAYMENT_VERIFIED | Трейдер подтвердил получение |
deal.completed | COMPLETED | Сделка завершена, средства зачислены |
deal.expired | EXPIRED | Заказ истёк по таймауту |
deal.appealed | APPEALED | Открыт диспут |
deal.refund_pending | REFUND_PENDING | Возврат средств инициирован |
deal.cancelled | CANCELLED | Игрок или мерч явно отменили сделку до подтверждения платежа |