Прием платежей (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; для обычной сделки можно опустить. |
target_bank | string | Нет | Канонический код банка реквизита (bank-routing): значение bank_name из GET /banks?method=.... Кнопка «1click Альфа» = метод + target_bank: alfa — каскад выдаёт реквизит только этого банка (strict-политика тенанта). Не передан — маршрутизация по методу как раньше. Неизвестный код → 400 с перечнем допустимых. |
payer_bank | string | Нет | Подсказка по банку ПЛАТЕЛЬЩИКА (free-text, отображение; не маршрутизирует — для маршрутизации есть target_bank). |
description | string | Нет | Описание платежа от мерчанта. Выводится клиенту на платёжной странице. |
callback_url | string | Нет | URL обработчика вебхуков для этой сделки (переопределяет адрес доставки webhook_url мерчанта; только PayIn). |
is_client_commission | bool | Нет | Режим комиссии: true — платит клиент; false (по умолчанию) — удерживается из суммы мерчанта. |
mode | string | Нет | Режим презентации сделки: "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)
- Создание сделки.
POST /deals/payinс"mode": "DIRECT"— набор остальных полей идентичен HOSTED. - Отрисовка. Покажите клиенту сумму (
amount), курс (exchange_rate_micro,amount_usdt) и реквизиты изrequisites; заведите таймер доrequisites.expires_at(равенpayment_timeout_atсделки). - Оплата. Клиент переводит средства в своём банке по реквизитам — канал, каскад и трейдер те же, что на hosted-странице.
- Подтверждение оплаты. Ваш сервер (или кнопка «Я оплатил» в вашем
UI) вызывает notify-payment — статус сделки →
PAYMENT_NOTIFIED. - Верификация и завершение. Трейдер подтверждает поступление, сделка
доводится до
COMPLETEDштатным циклом verify → complete — от вас действий не требуется. - Вебхуки. Приходит стандартный набор
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_type | string | Тип реквизита — то же имя и тот же словарь, что в requisite_types каталога GET /payment-methods: BANK_CARD — банковская карта, E_WALLET — электронный кошелёк, SBP — СБП (перевод по номеру телефона), SIM — мобильная коммерция (перевод на номер оператора связи), BANK_TRANSFER — международный банковский перевод. Это тип реквизита, а не платёжный метод: метод оплаты по-прежнему задаётся полем payment_method_id при создании сделки (например card_rub, sbp_rub). |
card_number | string | Полный PAN (без маскировки). Только requisite_type=BANK_CARD; у остальных — "". |
cardholder_name | string | ФИО держателя карты/счёта. Заполняется у BANK_CARD/BANK_TRANSFER; у остальных типов — если реквизит содержит имя держателя. |
bank_name | string | Отображаемое имя банка/канала (из каталога методов). |
phone | string | Номер телефона для перевода. Заполняется для E_WALLET/SBP/SIM. |
iban | string | Полный IBAN. Только requisite_type=BANK_TRANSFER; у остальных — "". |
qr_payload | string | Зарезервировано под QR; сегодня всегда "". |
requisite_id | string(UUID) | Идентификатор выданного реквизита (для сверки). |
expires_at | string(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_id | string(UUID) | Да | Идентификатор сделки (в пути URL). |
deal_hash | string | Да | 64-hex суффикс payment_url — capability плательщика; пустой или несовпадающий → 403 PERMISSION_DENIED. |
idempotency_key | string(UUID) | Да | Ключ уведомления: повтор с тем же ключом → 200 replay; новый ключ по сделке, уже получившей сигнал (PAYMENT_NOTIFIED) → 400 FAILED_PRECONDITION — не ретраить. |
tenant_id | string(UUID) | Нет | Резолвируется из самой сделки по order_id. |
file_ids | array | Нет | 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/*-эндпоинтов). Тело:
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
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 сделки и передаётся в вебхуках после верификации.
Допуск равен нулю. Автоматическое завершение возможно только при
точном совпадении сумм — до копейки, без округлений, комиссионных
или курсовых окон. Любое несовпадение (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_fiat | int64 | Да | Новая сумма в минорных единицах (> 0). |
reason | string | Да | Причина изменения — фиксируется в неизменяемом аудите дословно. |
idempotency_key | string(UUID) | Да | Ключ действия оператора: повтор с тем же ключом и той же суммой → replay; с другой суммой → 409. |
expected_amount_version | int64 | Нет | Оптимистичная блокировка (механизм вида 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
В режиме по умолчанию (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):
| Поле | Что содержит |
|---|---|
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 |
deal.cancelled | CANCELLED | Клиент или мерч явно отменили сделку до подтверждения платежа |