S
docs.syncra.money
API ReferenceMerchant API

Обратный вызов (Callbacks / Webhooks)

Получение и проверка подлинности серверных уведомлений о статусах платежей и выплат в Syncra V2

Серверные уведомления (Callbacks / Webhooks)

Для информирования вашей системы о смене статусов сделок, выводов и изменениях каталога платёжных методов шлюз Syncra V2 отправляет асинхронные HTTP POST-запросы на указанный вами адрес обратного вызова (Callback URL). Адрес доставки определяется иерархией:

  1. Per-deal callback_url — поле запроса CreateMerchantPayIn (POST /deals/payin): если задано, доставка для этой сделки идёт на него.
  2. merchant.webhook_url — адрес, настроенный на уровне мерчанта в админ-панели; используется, когда per-deal override не задан.
  3. Оба пусты — колбэк для сделки не создаётся (уведомление не отправляется).

Per-deal callback_urlтолько PayIn. В запросе POST /deals/payout такого поля нет: уведомления о выплатах всегда идут на merchant.webhook_url мерчанта.

callback_url переопределяет только адрес доставки. Он не отменяет предусловие создания сделки: webhook_url и webhook secret на аккаунте мерча обязательны ВСЕГДА (Stripe-модель: endpoint настраивается на аккаунте). Без аккаунт-level настройки создание сделки → 400 FAILED_PRECONDITION "merchant webhook URL is not configured".


История доставок (Delivery Log)

Каждая отправка вебхука фиксируется записью доставки. Мерчант читает свою историю через API (также доступна на вкладке «Вебхуки» кабинета):

GET /api/v1/p2p/merchant/callbacks

Канонический путь — внутри merchant-префикса. Историческое зеркало GET /api/v1/p2p/callbacks (без /merchant) продолжает работать для wire-compat и считается deprecated — новые интеграции стройте на каноническом пути. Поведение обоих биндингов идентично.

Параметры (Query)

ПараметрТипОписание
deal_idstring(UUID)Фильтр по сделке (опционально). Ветка deal_id возвращает весь трейл доставки этой сделки без пагинации.
page_sizeint32Строк на страницу (1–100, по умолчанию 20).
page_tokenstringНепрозрачный keyset-курсор (base64-JSON тапла created_at,id последней строки предыдущей страницы). Пустая строка = первая страница.

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

{
  "data": [
    {
      "callback_id": "8b23b294-8af5-4086-8ea3-2fa8569b30f6",
      "deal_id": "9512d99a-3bbc-4a32-acae-d6ad8bb7e32f",
      "merchant_id": "3e52c59c-3013-4efa-a2db-70cef2adc7ab",
      "callback_url": "https://merchant.example.com/webhook",
      "status": "SUCCESS",
      "retry_count": 1,
      "response_code": 200,
      "response_body": "{\"status\":\"ok\"}",
      "next_retry_at": "",
      "created_at": "2026-08-23T19:14:34Z"
    }
  ],
  "next_page_token": "eyJ0IjoiMjAyNi0wOC0yM1QxOToxNDozNC44MzE3OTVaIiwiaWQiOiJiMTFmN2ZiNC0zMTUzLTRlZDktOGExZS05MjNlNzlhMGQ4NTQifQ=="
}

next_page_token пуст = последняя страница. Курсор opaque — передавайте обратно как есть.

Поля записи Callback

ПолеТипОписание
callback_idstring(UUID)Идентификатор записи доставки.
deal_idstring(UUID)Сделка, о которой уведомление.
merchant_idstring(UUID)Мерчант-владелец.
callback_urlstringАдрес доставки (per-deal override или webhook_url мерчанта).
statusstringPENDING (в очереди) / SUCCESS (доставлено, 2xx) / RETRY (ошибка, ждёт авто-повтора) / ERROR (попытки исчерпаны — терминальное).
retry_countint32Число попыток доставки.
response_codeint32HTTP-код последнего ответа вашего эндпоинта.
response_bodystringТело последнего ответа (обрезается).
next_retry_atstring(RFC3339)Время следующей авто-попытки (пусто, если повторов не планируется).
created_atstring(RFC3339)Время создания записи.

Автоматическая лестница повторов (V2)

Успехом доставки считается любой ответ ниже 400 (включая 2xx и 3xx); ответ 4xx/5xx, сетевая ошибка или превышение webhook_timeout_sec (по умолчанию 10 с) — неудачей. Неудачная доставка повторяется по лестнице экспоненциальных пауз (пауза после N-й неудачной попытки):

Номер неудачной попыткиПауза до следующей попытки
15 s
230 s
35 min
415 min
5 и далее1 h

При лимите по умолчанию (max_retries = 5, per-merchant) доставка пробуется ровно 5 раз суммарно — исходная попытка плюс 4 повтора (паузы 5 s → 30 s → 5 min → 15 min); ступень 1 h достижима только при повышенном per-merchant max_retries. После исчерпания попыток запись переходит в терминальный ERROR и эмитится outbox-событие об исчерпании. Для api_version=v1 повторов нет — любая ошибка доставки терминальна (см. V1 compat ниже).

Тело ответа вашего эндпоинта сохраняется в историю доставок обрезанным до 2048 байт (response_body).


Ручной retry колбэка

Запись в терминальном ERROR (или зависшую в RETRY) можно пере-поставить в очередь немедленной доставки — аналог кнопки Retry в Stripe Dashboard (в кабинете — на вкладке «Вебхуки»):

POST /api/v1/p2p/merchant/callbacks/{callback_id}/retry

Тело запроса — пустой JSON-объект {}. Аутентификация: HMAC-пара или кабинетный JWT (dual-homed).

Правила (в порядке проверки)

  1. Только свой колбэк: чужой callback_id403 PERMISSION_DENIED (cannot access another merchant's resources), несуществующий → 404 (callback not found).
  2. Только ERROR / RETRY: для PENDING / SUCCESS400 FAILED_PRECONDITION (см. живой пример ниже).
  3. Cooldown 60 секунд на один колбэк (независимо от авто-попыток): повторный вызов раньше → 429 RESOURCE_EXHAUSTED.

Пример успешного ответа (200 OK)

Возвращается пере-поставленная запись: status=PENDING, next_retry_at≈now, поля ответа очищены, retry_count сохранён (ручной retry — дополнительная попытка в том же трейле; админский сброс, в отличие от мерчантского, счётчик обнуляет).

Живые ошибки стейджа

400 — колбэк не в состоянии ошибки (живой ответ):

{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "{\"code\":9,\"message\":\"callback is not in a retryable state (only ERROR and RETRY): status SUCCESS\",\"details\":[]}",
  "instance": "/api/v1/p2p-engine"
}

404 — неизвестный callback_id (живой ответ):

{
  "type": "about:blank",
  "title": "Not Found",
  "status": 404,
  "detail": "{\"code\":5,\"message\":\"callback not found: callback not found\",\"details\":[]}",
  "instance": "/api/v1/p2p-engine"
}

429 — cooldown 60 с:

{
  "type": "about:blank",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "{\"code\":8,\"message\":\"last manual retry 12s ago (cooldown 1m0s)\",\"details\":[]}",
  "instance": "/api/v1/p2p-engine"
}

Подпись запроса (Webhook Signature)

Каждый исходящий вебхук V2 подписывается тем же алгоритмом HMAC-SHA256, что используется для проверки входящих запросов мерчанта (merchantsig). Подпись передаётся в HTTP-заголовке:

  • V2: X-Signature: t={unix_timestamp},v1={hex_hmac_sha256}
  • V1 compat: SIGNATURE (HMAC-SHA512 от {startOfDayUTC};{salt})

В V2 заголовок называется X-Signature (НЕ X-Syncra-Signature). Реализация: callback_worker.go:201req.Header.Set("X-Signature", cb.Signature). Для legacy-мерчантов с api_version=v1 дополнительно выставляется заголовок SIGNATURE (без X--префикса).

Формат заголовка V2

X-Signature: t=1783856120,v1=9e87d0c3c8801d0a5198d023b6b19a16f2c8d2037920ab3e09819cd8e412cfab
  • t — Unix timestamp (UTC) момента формирования подписи.
  • v1 — hex HMAC-SHA256 от строки "{t}.{raw_body}", где raw_bodyсырое тело запроса (без нормализации), ключ — webhook_secret мерчанта.

Подпись и тело запроса гарантированно разделяют один байтовый источник: v1 вычисляется от байтов тела ровно в том виде, в котором они будут доставлены (фикс от 2026-08-22: ранее подпись могла считаться от сериализации, отличной от отправленных байтов, из-за чего HMAC, посчитанный мерчантом по полученному телу, не совпадал). Вердиктируйте по полученным байтам — совпадение будет.

V1 compat-формат (для мигрирующих мерчантов)

В legacy-вебхуках V1 временная метка подписи формируется строго как начало текущих суток в UTC (полночь, 00:00:00):

timestamp = DateTime.UtcNow.Date.ToUnixTimestamp()
signature = HMACSHA512("{timestamp};{salt}", merchantSecret)

V1 spec: повторных попыток нет (no retry) — любая ошибка доставки терминальна.


Формат уведомления V2

Тело — JSON, структура CallbackEventPayload (11 полей) для lifecycle-событий сделок/выплат/выводов:

{
  "event": "deal.completed",
  "deal_id": "7ac148fe-19a3-45bb-b992-019fac55b721",
  "merchant_id": "8f870a31-b88d-4cb0-a882-628d08cbda88",
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
  "amount": 250050,
  "currency": "RUB",
  "status": "COMPLETED",
  "timestamp": "2026-07-12T17:24:12Z",
  "received_amount": 250050,
  "amount_match_status": "full",
  "provider_metadata": {
    "bank_code": "sbp",
    "rrn": "123456789012"
  }
}

Исключение — событие каталога payment_methods.updated: для него используется отдельный конверт без полей сделки (amount, currency, deal_id) — см. раздел События каталога платёжных методов.

Поля CallbackEventPayload

ПолеТипОбяз.Описание
eventstringДаИмя события (см. таблицу ниже).
deal_idstring(UUID)ДаИдентификатор сделки (Order.ID или PayOut.ID для payout-событий, withdrawal_id для withdrawal-событий).
merchant_idstring(UUID)ДаИдентификатор мерчанта — владельца сделки.
tenant_idstring(UUID)ДаИдентификатор тенанта.
amountint64ДаСумма в минорных единицах (копейки/центы/satoshi).
old_amountint64НетСумма ДО изменения — только в deal.amount_changed.
amount_versionint64НетМонотонный счётчик правок суммы — только в deal.amount_changed (начальное значение сделки — 1, каждая правка +1).
currencystringДаISO 4217 (RUB, KZT, ...). Для USDT-выводов — USDT.
statusstringДаТекущий canonical статус: OrderStatus (PayIn), PayOutStatus (PayOut), WithdrawalStatus (вывод).
timestampstring(RFC3339)ДаUTC-время формирования события.
received_amountint64НетФактически полученная от плательщика сумма. Только для PayIn после верификации.
amount_match_statusstringНетРезультат сверки сумм при PayIn: full / partial / overpaid (строго lowercase).
provider_metadataobjectНетПровайдер-специфичные метаданные (чувствительные данные вроде PAN редуцируются).

Все int64 в телах вебхуков передаются строками — тот же канон, что и в REST-ответах (proto3 JSON): "amount": "330000", "amount_version": "2". Один парсер сумм на всю интеграцию; голое JSON-число нигде не приходит. В V1-конверте формат наследует легаси-контракт V1.

amount_match_status передаётся строго lowercase: full, partial, overpaid. Значения MATCHED, UNDERPAID, OVERPAID (uppercase) в V2 не существуют — это legacy-артефакты.

Платёжные реквизиты в вебхуках не передаются — никогда. Тела вебхуков (V2 CallbackEventPayload и legacy V1-конверт) содержат только идентификаторы, суммы, валюту и статус: полные PAN/IBAN/телефоны мерчанту не отправляются (PII-минимизация). Исчерпывающий источник реквизитов для PayIn — публичный GET /api/v1/p2p/checkout/{hash} (см. SDK Examples §9). В V1-конверте поле cardNumber приходит только замаскированным (**** **** **** 1234 — последние 4 цифры); в provider_metadata чувствительные значения (PAN, IBAN, номера телефонов) редуцируются перед отправкой.


Типы событий (event)

События PayIn/PayOut используют префикс deal.* (мерчантский вебхук) или p2p.deal.* (внутренняя Kafka). События вывода USDT — отдельное имя.

События PayIn (OrderStatus transitions)

Имя событияСтатус сделкиКогда отправляется
deal.createdINITIALIZEDPayIn-сделка создана.
deal.escrow_lockedESCROW_LOCKEDЭскроу-холд зарезервирован, реквизиты выданы.
deal.payment_notifiedPAYMENT_NOTIFIEDКлиент нажал «Я оплатил».
deal.amount_changed(без смены статуса)Оператор изменил сумму живой сделки (до PAYMENT_VERIFIED). Payload несёт old_amount, новую amount и amount_version. Только V2 — см. примечание ниже.
deal.payment_verifiedPAYMENT_VERIFIEDПоступление средств подтверждено, сумма сверена.
deal.completedCOMPLETEDPayIn финально завершён успешно.
deal.expiredEXPIREDСделка истекла по payment_timeout_at.
deal.appealedAPPEALEDПо сделке открыта апелляция.
deal.refund_pendingREFUND_PENDINGИнициирован возврат средств.
deal.cancelledCANCELLEDИгрок или мерч явно отменили сделку до подтверждения платежа.

deal.amount_changed доступно только мерчантам V2: legacy-конверт V1 не имеет callbackType под это событие (маппер знает только коды статусов сделок 1/2/11/12/13/14/15/16/21), поэтому V1-мерчантам оно не доставляется — как и события каталога. Это контрактное поведение, а не пропуск. Settlement-сделка (зачёт просроченного платежа) эмитит обычный набор событий deal.createddeal.completed — эти события доступны обоим версиям API, различить сеттлмент можно по полям parent_deal_id / origin_appeal_id в объекте сделки.

События PayOut (PayOutStatus transitions)

PayOut-события используют те же имена deal.completed / deal.failed, что и PayIn (НЕ payout.completed). Это намеренно: PayOut — это тоже «сделка» в терминах вебхука. Различить направление можно по полю status (PayOutStatus vs OrderStatus) или по отсутствию полей received_amount / amount_match_status (они есть только у PayIn).

Имя событияСтатус выплатыКогда отправляется
deal.createdINITIALIZEDPayOut создан, баланс захолдирован.
deal.completedCOMPLETEDPayOut успешно исполнён — средства отправлены получателю.
deal.failedFAILEDPayOut отклонён (неверные реквизиты, отмена трейдером). Холд возвращён мерчанту.
deal.expiredEXPIREDPayOut истёк до завершения перевода.

События вывода USDT (WithdrawalStatus)

Имя событияСтатусКогда отправляется
p2p.finance.withdrawal_completedAPPROVEDOn-chain tx отправлена, ledger списан.
p2p.finance.withdrawal_failedCANCELLEDВывод отклонён (admin / pre-broadcast cancel).

В Kafka эти же переходы публикуются с префиксом p2p.deal.* для внутренних потребителей. Мерчантские вебхуки V2 — deal.* без префикса; вывод средств — p2p.finance.withdrawal_*.

События каталога платёжных методов

Имя событияКогда отправляется
payment_methods.updatedАдминистратор создал или изменил каталог-видимые поля метода оплаты (name, status, requisite_types, is_cross_border). Рассылка всем мерчантам тенанта.

payment_methods.updatedтолько V2 (api_version = v2). Legacy-конверт V1 не имеет типа уведомления для событий каталога (callbackType 1/2/3/10), поэтому V1-мерчантам событие не отправляется: доставка только активным V2 мерчантам с настроенным непустым webhook_url.

Обновление со идентичными значениями (no-op) НЕ эмитится.

Конверт каталог-события — отдельная структура (НЕ CallbackEventPayload): поля amount / currency / deal_id в нём отсутствуют — событие описывает изменение справочника, а не транзакцию:

{
  "event": "payment_methods.updated",
  "merchant_id": "8f870a31-b88d-4cb0-a882-628d08cbda88",
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
  "timestamp": "2026-08-27T12:00:00Z",
  "data": {
    "changed": [
      {
        "payment_method_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "slug": "sbp_rub",
        "status": "DISABLED",
        "name": "SBP",
        "before": {
          "status": "ACTIVE",
          "name": "SBP",
          "requisite_types": ["SBP"]
        }
      }
    ]
  }
}

Элемент data.changed[] несёт AFTER-состояние метода (payment_method_id, slug, status, name); при обновлении дополнительно before — снапшот каталог-видимых полей до изменения (status, name, requisite_types, is_cross_border). При создании метода before отсутствует.

Событие — нотификация, а не источник данных. Рекомендация: по payment_methods.updated перечитать актуальный каталог через GET /api/v1/p2p/merchant/payment-methods и закэшировать до следующего события. Не стройте локальную копию каталога только из payload — полный список методов событие не содержит.

Доставка и подпись — те же, что у lifecycle-событий: X-Signature (HMAC-SHA256 от {t}.{raw_body} ключом webhook_secret мерчанта), повторы по лестнице выше. Запись доставки в истории (GET /api/v1/p2p/merchant/callbacks) не привязана ни к сделке, ни к выплате (соответствующие поля NULL).


Примеры payload по событиям

deal.amount_changed — оператор изменил сумму живой сделки (до верификации платежа). amount несёт НОВУЮ сумму, old_amount — прежнюю, amount_version — номер правки (монотонный, стартует с 1 у новой сделки): эскроу-холды пересозданы под новую сумму, USDT-эквивалент пересчитан по курсу самой сделки. Статус сделки при этом не меняется:

{
  "event": "deal.amount_changed",
  "deal_id": "7ac148fe-19a3-45bb-b992-019fac55b721",
  "merchant_id": "8f870a31-b88c-4cb0-a882-628d08cbda88",
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
  "amount": 200000,
  "old_amount": 150000,
  "amount_version": 2,
  "currency": "RUB",
  "status": "PAYMENT_NOTIFIED",
  "timestamp": "2026-09-03T18:12:44Z"
}

deal.payment_verified — поступление средств подтверждено (сверка сумм):

{
  "event": "deal.payment_verified",
  "deal_id": "7ac148fe-19a3-45bb-b992-019fac55b721",
  "merchant_id": "8f870a31-b88d-4cb0-a882-628d08cbda88",
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
  "amount": 250050,
  "currency": "RUB",
  "status": "PAYMENT_VERIFIED",
  "timestamp": "2026-07-12T17:23:45Z",
  "received_amount": 250050,
  "amount_match_status": "full"
}

deal.completed (PayIn) — успешное завершение входящего платежа:

{
  "event": "deal.completed",
  "deal_id": "7ac148fe-19a3-45bb-b992-019fac55b721",
  "merchant_id": "8f870a31-b88d-4cb0-a882-628d08cbda88",
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
  "amount": 250050,
  "currency": "RUB",
  "status": "COMPLETED",
  "timestamp": "2026-07-12T17:24:12Z",
  "received_amount": 250050,
  "amount_match_status": "full"
}

deal.expired (PayIn) — сделка истекла по payment_timeout_at, средства не поступили (терминальный неуспешный исход входящего платежа). Поля received_amount и amount_match_status отсутствуют — сверка сумм не выполнялась:

{
  "event": "deal.expired",
  "deal_id": "7ac148fe-19a3-45bb-b992-019fac55b721",
  "merchant_id": "8f870a31-b88d-4cb0-a882-628d08cbda88",
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
  "amount": 250050,
  "currency": "RUB",
  "status": "EXPIRED",
  "timestamp": "2026-07-12T17:55:00Z"
}

deal.cancelled (PayIn) — сделка явно отменена (игроком на платёжной странице или мерчантом через cancel-API) до подтверждения платежа. Как и у deal.expired, терминальный неуспешный исход: поля received_amount и amount_match_status отсутствуют — сверка сумм не выполнялась:

{
  "event": "deal.cancelled",
  "deal_id": "7ac148fe-19a3-45bb-b992-019fac55b721",
  "merchant_id": "8f870a31-b88d-4cb0-a882-628d08cbda88",
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
  "amount": 250050,
  "currency": "RUB",
  "status": "CANCELLED",
  "timestamp": "2026-07-12T17:30:00Z"
}

Если ваш обработчик фильтрует события по имени (например, реагирует только на deal.completed), добавьте deal.cancelled в фильтр — иначе отмена пройдёт мимо вашей логики, а сделка «зависнет» в ожидании финала.

deal.completed (PayOut) — успешное завершение выплаты (средства отправлены получателю). Обратите внимание: received_amount и amount_match_status отсутствуют — это поля только для PayIn:

{
  "event": "deal.completed",
  "deal_id": "e81d77a0-0d3a-4422-b91c-7cb052a2119c",
  "merchant_id": "8f870a31-b88d-4cb0-a882-628d08cbda88",
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
  "amount": 500000,
  "currency": "RUB",
  "status": "COMPLETED",
  "timestamp": "2026-07-12T17:28:00Z"
}

deal.failed (PayOut) — выплата отклонена, холд возвращён:

{
  "event": "deal.failed",
  "deal_id": "e81d77a0-0d3a-4422-b91c-7cb052a2119c",
  "merchant_id": "8f870a31-b88d-4cb0-a882-628d08cbda88",
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
  "amount": 500000,
  "currency": "RUB",
  "status": "FAILED",
  "timestamp": "2026-07-12T17:29:30Z"
}

p2p.finance.withdrawal_completed — криптовалютный вывод USDT исполнен:

{
  "event": "p2p.finance.withdrawal_completed",
  "deal_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "merchant_id": "8f870a31-b88d-4cb0-a882-628d08cbda88",
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
  "amount": 1000000,
  "currency": "USDT",
  "status": "APPROVED",
  "timestamp": "2026-07-12T17:30:05Z"
}

Для withdrawal-событий поле deal_id содержит идентификатор withdrawal-записи (заявки на вывод USDT), а не Order.ID — вывод не является ордером.


Верификация подписи V2

const crypto = require('crypto');

function verifyV2Webhook(rawBody, signatureHeader, webhookSecret) {
  // 1. Парсинг заголовка X-Signature: t=<unix>,v1=<hex>
  const parts = signatureHeader.split(',');
  const timestampPart = parts.find(p => p.startsWith('t='));
  const signaturePart = parts.find(p => p.startsWith('v1='));

  if (!timestampPart || !signaturePart) return false;

  const timestamp = timestampPart.substring(2);
  const providedSignature = signaturePart.substring(3);

  // 2. Защита от replay-атак: метка не старше 5 минут
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp, 10)) > 300) {
    return false;
  }

  // 3. Вычисление ожидаемой сигнатуры: HMAC-SHA256("{t}.{body}", secret)
  const payload = `${timestamp}.${rawBody}`;
  const expectedSignature = crypto
    .createHmac('sha256', webhookSecret)
    .update(payload)
    .digest('hex');

  // 4. Безопасное сравнение (timing-safe)
  return crypto.timingSafeEqual(
    Buffer.from(providedSignature, 'utf-8'),
    Buffer.from(expectedSignature, 'utf-8')
  );
}

Подпись вычисляется по сырому телу запроса (raw bytes), как оно пришло из сети. Не декодируйте/нормализуйте JSON перед верификацией — это сломает подпись.


Типы уведомлений в адаптере (V1 Compatibility)

Для legacy-мерчантов с api_version=v1 callback-service строит V1-конверт:

{
  "callbackType": 1,
  "callbackData": {
    "paymentId": "7ac148fe-19a3-45bb-b992-019fac55b721",
    "payInId": "order_12345",
    "clientId": "user_99",
    "status": 11,
    "currency": 643,
    "amount": "2500.50",
    "paidAmount": 2500.50,
    "cardNumber": "**** **** **** 1234",
    "fullName": "Иван И.",
    "callbackUrl": "https://example.com/webhook",
    "createdAt": "2026-07-12T17:24:12Z"
  },
  "salt": 4729103
}

Маппинг типов уведомлений V1 (callbackType)

callbackTypeСущностьСоответствие V2
1Входящий платеж (PayIn)deal.* с OrderStatus
2Исходящий платеж (PayOut)deal.completed / deal.failed с PayOutStatus
3Открытие/изменение спораdeal.appealed
10Криптовывод USDTp2p.finance.withdrawal_*

V1 spec: NO RETRIES. Для api_version=v1 любая неудача доставки терминальна — повторных попыток нет. Экспоненциальный backoff ниже применяется только к V2 мерчантам.


Правила обработки вебхуков (Best Practices)

  1. Возвращайте HTTP 200 быстро. Обработчик должен сразу вернуть 200 OK с телом {"status":"success"}. Тяжёлую бизнес-логику выносите в фоновую очередь — сначала сохраните событие, ответьте 200, затем обрабатывайте асинхронно.
  2. Идемпотентность. Всегда проверяйте, не обрабатывали ли вы уже событие с данным deal_id + event + timestamp. Дубли возможны из-за ретраев.
  3. Игнорируйте неизвестные типы событий. Список event расширяется со временем (новые статусы, новые сущности) — ваш обработчик должен сохранять/логировать неизвестный event и отвечать 200, а не падать или возвращать ошибку. Отвечать ошибкой имеет смысл только когда вы действительно хотите повторную доставку.
  4. Retry Policy (V2). Автоматическая лестница повторов описана выше (раздел История доставок); терминальный ERROR можно пере-поставить в очередь ручным retry.
  5. IP Whitelisting. Для дополнительной защиты можно ограничить приём вебхуков списком IP-адресов шлюза Syncra (актуальный список — в панели управления).

On this page