S
docs.syncra.money
API ReferenceMerchant API

Коды ошибок

Обработка ошибок, форматы ответов при сбоях, справочник кодов и retry-матрица в Syncra Merchant API V2

Обработка ошибок в API

При обработке запросов, содержащих невалидные параметры, некорректную подпись или выполняемых при нехватке средств на балансе, Syncra Merchant API V2 возвращает ошибку с соответствующим HTTP-кодом состояния и телом ответа в формате RFC 9457 Problem Details (Content-Type: application/problem+json).

Все примеры на этой странице — реальные ответы стейджа (api-stage.syncra.money), полученные живыми запросами.


Формат тела ошибки (Error Shape)

Каждый non-2xx ответ транслируется из gRPC в единый формат problem+json. Реальный ответ (GET /deals?page_token=garbage-token):

HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
X-Trace-Id: 4f7c1a9b-8e2d
{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "{\"code\":3,\"message\":\"invalid page_token\",\"details\":[]}",
  "instance": "/api/v1/p2p-engine"
}

Структура ответа (RFC 9457)

  • type (строка, URI): идентификатор типа ошибки. Для всех transcoded-ответов p2p-engine — about:blank (тип описывается status + title).
  • title (строка): стандартный текст HTTP-статуса (Bad Request, Unauthorized, Not Found, Too Many Requests).
  • status (число): HTTP-код состояния.
  • detail (строка): вложенный JSON gRPC-деталей — строка вида {"code":<grpc_code>,"message":"<текст>","details":[]}. Именно message внутри detail несёт человекочитаемое описание; code — числовой gRPC-код (3 = INVALID_ARGUMENT, 5 = NOT_FOUND, 9 = FAILED_PRECONDITION, 14/16 = UNAUTHENTICATED, 8 = RESOURCE_EXHAUSTED). Для программной обработки парсите detail как JSON.
  • instance (строка): идентификатор обработавшего запрос upstream-сервиса (/api/v1/p2p-engine), а не путь исходного запроса.

Идентификатор трассировки: заголовок X-Trace-Id

В теле problem+json нет trace-полей (никаких расширений trace_id / traceId). Идентификатор трассировки передаётся заголовком X-Trace-Id (Stripe-канон), который несёт каждый ответ (и успешный, и с ошибкой):

  • именно значение X-Trace-Id называйте в обращениях в поддержку — по нему поддержка находит запрос в логах;
  • для браузерных клиентов заголовок экспонируется через CORS (Access-Control-Expose-Headers), т.е. доступен из JavaScript (response.headers.get('X-Trace-Id')); без expose браузерный JS не видит кастомные заголовки.

Дополнительно каждый успешный ответ несёт заголовки X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset (Unix-секунды сброса окна).

Отдельного JSON-поля code верхнего уровня в теле ошибки нет. Числовой gRPC-код вложен в detail (см. выше). Для программной обработки используйте status и detail.


Канон маппинга HTTP-кодов

Неверное состояние запроса → 400 Bad Request (не 409). Все классы FAILED_PRECONDITION — недостаток баланса, превышение дневного лимита, повторная апелляция, retry невалидного колбэка, ненастроенный вебхук — отдаются как 400. Единственный 409 Conflict в API V2 — конкурентный резерв whitelist-pass двумя сделками (см. Passes).

API V2 транслирует gRPC-коды состояния бэкенда в стандартные HTTP-коды:

HTTP КодgRPC-код (detail.code)Типовая ситуация
4003 INVALID_ARGUMENTОшибка валидации параметров (неверный формат суммы, отсутствующие поля, битый page_token, пустой/неизвестный payment_method_id, метод чужой валюты).
4009 FAILED_PRECONDITIONНеверное состояние: недостаточно средств, дневной лимит, активная апелляция, вебхук не настроен на аккаунте, retry недоступен.
40116 UNAUTHENTICATEDНет заголовков подписи, неверный токен, кривая/истёкшая подпись.
4037 PERMISSION_DENIEDЧужой ресурс (deal/appeal/callback другого мерчанта), Kill-Switch, блокировка.
4045 NOT_FOUNDРесурс не найден (несуществующий deal_id, appeal_id, callback_id, маршрут).
40910 ABORTEDКонкурентный резерв whitelist-pass (единственный 409-путь; retry безопасен).
4298 RESOURCE_EXHAUSTEDRate limit токена; cooldown ручного retry колбэка (60 с); velocity-защита.
50013 INTERNALНепредвиденный внутренний сбой. Повторите попытку позже.
50314 UNAVAILABLEНет свободного трейдера для матчинга (detail: "...no matching trader available, retry later"). Повторите позже с backoff.

Живые примеры по кодам

400 — валидация (INVALID_ARGUMENT)

Пустой payment_method_id (обязательное поле с 2026-08-24):

{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "{\"code\":3, \"message\":\"payment_method_id is required (UUID or slug from GET /payment-methods)\", \"details\":[]}",
  "instance": "/api/v1/p2p-engine"
}

Битый page_token в листинге сделок:

{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "{\"code\":3,\"message\":\"invalid page_token\",\"details\":[]}",
  "instance": "/api/v1/p2p-engine"
}

Неизвестная ссылка на метод оплаты (PayIn payment_method_id / PayOut payment_method; slug и UUID резолвятся одинаково) — в detail перечислены допустимые слаги активных методов:

{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "{\"code\":3,\"message\":\"payment_method_id: not found: requested \\\"sbp_try\\\", allowed slugs: sbp_rub, card_rub, mobcom_rub\",\"details\":[]}",
  "instance": "/api/v1/p2p-engine"
}

Метод другой валюты (PayOut: резолв успешен, но метод обслуживает не ту валюту сделки):

{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "{\"code\":3,\"message\":\"merchant_api: payment method currency does not match the deal currency: method \\\"card_rub\\\" serves another currency\",\"details\":[]}",
  "instance": "/api/v1/p2p-engine"
}

Нарушение лимитов сделок при создании PayIn (минимум/максимум — INVALID_ARGUMENT):

{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "{\"code\":3,\"message\":\"deal amount is below the merchant minimum: 50000 is below the configured minimum 100000\",\"details\":[]}",
  "instance": "/api/v1/p2p-engine"
}

Отсутствующее обязательное поле (reason апелляции, body сообщения, wallet_id выплаты):

{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "{\"code\":3,\"message\":\"reason is required\",\"details\":[]}",
  "instance": "/api/v1/p2p-engine"
}

400 — неверное состояние (FAILED_PRECONDITION)

Дневной лимит оборота сделок (см. Лимиты мерча):

{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "{\"code\":9,\"message\":\"merchant daily deal volume limit exceeded: today's volume 793921 + requested 150000 exceeds the configured daily cap 100000\",\"details\":[]}",
  "instance": "/api/v1/p2p-engine"
}

Повторная апелляция по сделке с уже активной апелляцией — продолжайте существующую ветку:

{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "{\"code\":9,\"message\":\"deal 359fb026-6dd7-4f7a-bfa7-aa54a5afb6cb already has an active appeal (aa1fac74-33b3-41ce-ae47-445bc3dcd37d, status OPEN) — continue the existing thread\",\"details\":[]}",
  "instance": "/api/v1/p2p-engine"
}

Ручной retry колбэка, не находящегося в состоянии ошибки (допустимы только ERROR / RETRY):

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

400 — конфигурация вебхука (FAILED_PRECONDITION)

PayIn/PayOut отклоняются, пока у мерчанта не настроен вебхук:

Условиеdetail (message)
Не задан webhook URLmerchant webhook url is not configured
Не инициализирован webhook secretmerchant webhook secret is missing

Настройте вебхук в кабинете (webhook_url в Профиле мерчанта) или в админ-панели.

401 — аутентификация (UNAUTHENTICATED)

Причинами возврата 401 могут быть (живые ответы):

  • Нет HMAC-заголовков вообще (или JWT для кабинетных эндпоинтов):
{
  "type": "about:blank",
  "title": "Unauthorized",
  "status": 401,
  "detail": "{\"code\":16,\"message\":\"missing authorization header\",\"details\":[]}",
  "instance": "/api/v1/p2p-engine"
}
  • Неверный токен, неверная подпись или истёкший X-Timestamp — единый ответ (replay-окно: подпись старше 5 минут или из будущего дальше 60 секунд отклоняется — суммарный допуск |Δt| ≤ 6 минут; проверьте NTP-синхронизацию и что подпись считается от "{t}.{raw_body}"):
{
  "type": "about:blank",
  "title": "Unauthorized",
  "status": 401,
  "detail": "{\"code\":16,\"message\":\"invalid merchant credentials\",\"details\":[]}",
  "instance": "/api/v1/p2p-engine"
}

Поле merchant_id в теле запроса при HMAC-аутентификации — advisory: реальный скоуп всегда берётся из подписанного токена (claims-win). Невалидный формат или чужой merchant_id в запросе не вызывает ошибку — значение молча игнорируется, вызов выполняется от имени мерчанта, которому принадлежит токен. Исключение — запросы с cabinet-JWT-аутентификацией (личный кабинет): там несоответствие merchant_id отклоняется с 403 Forbidden.

403 — чужой ресурс (PERMISSION_DENIED)

Обращение к deal_id / appeal_id / callback_id, принадлежащему другому мерчанту (IDOR-защита; для GET /deals/{id} чужие идентификаторы схлопываются в 404, для мутирующих операций — 403):

{
  "type": "about:blank",
  "title": "Forbidden",
  "status": 403,
  "detail": "{\"code\":7,\"message\":\"cannot access another merchant's resources\",\"details\":[]}",
  "instance": "/api/v1/p2p-engine"
}

Также 403 возвращается при заблокированном мерчанте или включённом Kill-Switch.

404 — не найдено (NOT_FOUND)

Несуществующий deal_id:

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

429 — rate limit и cooldown (RESOURCE_EXHAUSTED)

  • Лимит запросов токена — счётчик per-merchant (по умолчанию 10 000/мин, настраивается; на стейдж-песочнице окно шире). Окно — календарная минута UTC, заголовки X-RateLimit-* на каждом ответе подсказывают остаток:
{
  "type": "about:blank",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "{\"code\":8,\"message\":\"merchant rate limit exceeded (limit: 10000/min)\",\"details\":[]}",
  "instance": "/api/v1/p2p-engine"
}
  • Cooldown ручного retry вебхука — не чаще одного ретрая на колбэк за 60 секунд (Retry-After не передаётся; пауза называется в тексте detail — см. Callbacks: ручной retry):
{
  "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"
}
  • Velocity-защита (antifraud): превышен лимит PayIn-запросов на один client_id (максимум 10 в час) — см. Antifraud.

Antifraud-ошибки

Эти ошибки возникают при срабатывании защитных механизмов (см. Antifraud):

HTTP КодКод (семантика)Описание ошибки
429CLIENT_RATE_LIMITEDСработала velocity-защита: клиент превысил лимит PayIn-запросов (максимум 10 в час на один client_id).
403CUSTOMER_BLOCKEDКлиент заблокирован (IsBlocked = true и блокировка не истекла). Создание платежей и доступ к checkout запрещены.

Ошибки валидации (INVALID_ARGUMENT) — сводка

Этот код возвращается, если параметры запроса не соответствуют бизнес-правилам шлюза. Примеры:

  • Значение amount отрицательное или равно нулю.
  • Значение currency передано в неверном формате или не поддерживается.
  • Обязательное поле idempotency_key отсутствует или пустое (формат ключа не валидируется — см. Идемпотентность).
  • Реквизиты в target_requisite содержат невалидный формат (буквенные символы в номере карты, неверный формат телефона для СБП).
  • Передан некорректный адрес криптовалютного кошелька для вывода средств в USDT.
  • Битый/просроченный page_token пагинации (invalid page_token).
  • Неверный date_from/date_to в оборотах (не RFC3339, окно > 92 дней, date_to раньше date_from).
  • Сумма сделки вне собственных лимитов мерчанта (min/max — см. Лимиты мерча).

Retry-матрица: когда повторять запрос безопасно

Не все ошибки можно безопасно повторять. Используйте эту матрицу, чтобы избежать дублей и усугубления лимитов:

Тип запросаКогда retry безопасенКогда retry НЕ безопасен
GET (любой)Всегда безопасно. GET-запросы идемпотентны по природе.
POST с idempotency_keyБезопасно: повтор с тем же ключом вернёт исходный результат без создания дубля.
POST без idempotency_keyНебезопасно. Запрос мог пройти на бэкенде; повтор создаст дубль.Избегайте: всегда передавайте idempotency_key (см. Идемпотентность).

Поведение при конкретных кодах

Код ошибкиПовторять?Стратегия
UNAUTHENTICATED (401)НетНе ретрать без исправления подписи/токена.
INVALID_ARGUMENT (400)НетНе ретрай — исправьте тело запроса.
FAILED_PRECONDITION (400)НетПополните баланс / устраните предусловие / продолжите существующую апелляцию. Включая повторный notify-payment по сделке, уже получившей «я оплатил» (статус PAYMENT_NOTIFIED): повтор не ретраить — состояние сделки корректно, дождитесь вебхука/резолва оператора.
PERMISSION_DENIED / CUSTOMER_BLOCKED (403)НетУсловие не изменится от повтора.
NOT_FOUND (404)НетРесурс не появится от повтора.
RATE_LIMIT / RESOURCE_EXHAUSTED (429)Да, с backoffЛимит/окно; повтор после сброса окна (X-RateLimit-Reset) или экспоненциальный backoff.
INTERNAL (500)Да, с backoffВременный сбой; повтор через экспоненциальный backoff.
UNAVAILABLE (503)Да, с backoffНет свободного трейдера; повтор через экспоненциальный backoff.

Для 429, 500 и 503 используйте экспоненциальный backoff с джиттером (например, 1с → 2с → 4с → 8с). Для ручного retry вебхука дождитесь истечения 60-секундного cooldown.


Связанные материалы

On this page