Коды ошибок
Обработка ошибок, форматы ответов при сбоях, справочник кодов и 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) | Типовая ситуация |
|---|---|---|
| 400 | 3 INVALID_ARGUMENT | Ошибка валидации параметров (неверный формат суммы, отсутствующие поля, битый page_token, пустой/неизвестный payment_method_id, метод чужой валюты). |
| 400 | 9 FAILED_PRECONDITION | Неверное состояние: недостаточно средств, дневной лимит, активная апелляция, вебхук не настроен на аккаунте, retry недоступен. |
| 401 | 16 UNAUTHENTICATED | Нет заголовков подписи, неверный токен, кривая/истёкшая подпись. |
| 403 | 7 PERMISSION_DENIED | Чужой ресурс (deal/appeal/callback другого мерчанта), Kill-Switch, блокировка. |
| 404 | 5 NOT_FOUND | Ресурс не найден (несуществующий deal_id, appeal_id, callback_id, маршрут). |
| 409 | 10 ABORTED | Конкурентный резерв whitelist-pass (единственный 409-путь; retry безопасен). |
| 429 | 8 RESOURCE_EXHAUSTED | Rate limit токена; cooldown ручного retry колбэка (60 с); velocity-защита. |
| 500 | 13 INTERNAL | Непредвиденный внутренний сбой. Повторите попытку позже. |
| 503 | 14 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 URL | merchant webhook url is not configured |
| Не инициализирован webhook secret | merchant 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 Код | Код (семантика) | Описание ошибки |
|---|---|---|
| 429 | CLIENT_RATE_LIMITED | Сработала velocity-защита: клиент превысил лимит PayIn-запросов (максимум 10 в час на один client_id). |
| 403 | CUSTOMER_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.
Связанные материалы
- Antifraud — velocity-защита, блокировка клиентов, сегментация.
- Идемпотентность — почему retry безопасен только с
idempotency_key. - Аутентификация — расчёт подписи
X-Signature. - Callbacks — авто-лестница ретраев вебхуков и ручной retry.