Ошибки
Каждый ответ 4xx/5xx несёт один и тот же JSON-конверт:
{
"code": 400,
"message": "invalid_input",
"details": [
{ "field": "items[0].unit_price", "message": "must be a positive decimal string" }
]
}code— числовой HTTP-статус (400,401,404, …). Полезен для общей обработки на HTTP-уровне, но для логики ветвления переключайтесь поmessage—codeне разделит, например,invalid_inputиpayment_method_not_supported(оба 400).message— sentinel-имя в нижнем snake_case, выведенное из внутренней константыerrors.go(например,INVALID_INPUT → "invalid_input"). Стабильно между релизами — переключайтесь по нему.details— заполняется при ошибках валидации. Массив объектов{ field, message }, чтобы клиент мог привязать ошибки к полям ввода. В остальных случаях не указывается.
Мы сейчас не возвращаем trace_id или timestamp в теле. Ранние
черновики этой страницы обещали оба — это было намерением, а не
реальностью. Если вам нужно сопоставить серверный лог с запросом,
снимите заголовок Date из ответа и серверные rate-limit заголовки
(X-RateLimit-*) и приложите их к тикету в поддержку.
Отклонения на кромке gateway используют другую форму. Конверт
выше — то, что отправляют backend-сервисы. Запросы, отклонённые на
gateway до достижения сервиса — отсутствующая/некорректная
X-Signature, неизвестный X-Client-ID или устаревший X-Timestamp
на вызове /b2b/v1/* — приходят как
{ "error": "...", "message": "..." }, где error — грубый slug
(unauthorized / bad_request / service_unavailable), а message
несёт конкретику. Нет числового code и нет details. Поэтому
проверяющая сторона должна сначала ветвиться по HTTP-статусу, потом
читать message, и трактовать code/details как присутствующие
только когда запрос прошёл gateway. Пример тела gateway (401):
{ "error": "unauthorized", "message": "invalid signature" }HTTP-статус → типичные коды
| HTTP | Типичные значения code | Что значит |
|---|---|---|
| 400 | INVALID_INPUT, MISSING_REQUIRED, INVALID_FORMAT, INVALID_LENGTH, INVALID_VALUE, INVALID_USER_STATUS, INVALID_USER_ROLE, PAYMENT_METHOD_NOT_SUPPORTED, AMOUNT_BELOW_MINIMUM | Некорректный запрос — смотрите details |
| 401 | INVALID_CREDENTIALS, INVALID_TOKEN, TOKEN_EXPIRED, INVALID_SIGNATURE (B2B); INVALID_OTP, OTP_EXPIRED, SESSION_EXPIRED (только панель / JWT) | Аутентификация не прошла — неверный ключ, истёкший timestamp, неправильная подпись. Коды OTP/SESSION_EXPIRED появляются только на маршрутах JWT-панели (/payment/v1/*); чистые B2B-интеграции их не увидят. |
| 402 | INSUFFICIENT_CREDIT | Предоплаченный баланс мерчанта закончился — пополните перед повтором |
| 403 | FORBIDDEN, IP_BLOCKED | Ключ валиден, но у него нет scope’а / разрешения по IP для этого вызова |
| 404 | NOT_FOUND, RECORD_NOT_FOUND, USER_NOT_FOUND, SESSION_NOT_FOUND | Ресурс не существует (или не существует для этого мерчанта) |
| 409 | ALREADY_EXISTS, USER_ALREADY_EXISTS, SESSION_ALREADY_EXISTS | Idempotency-replay с другим телом, либо state-машина отказала в переходе |
| 429 | TOO_MANY_REQUESTS, TOO_MANY_ATTEMPTS | Достигнут rate-limit; сделайте отступ и повторите |
| 500 | INTERNAL_SERVER_ERROR, DATABASE_CONNECTION_ERROR, DATABASE_QUERY_ERROR, DATABASE_TRANSACTION_ERROR, REDIS_CONNECTION_ERROR, REDIS_OPERATION_ERROR, EXTERNAL_SERVICE_ERROR | Наша вина; безопасно повторить с отступом. (Provider-специфичные коды вроде TWILIO_SERVICE_ERROR / SENDGRID_SERVICE_ERROR существуют внутри, но всплывают только на потоках уведомлений со стороны панели, не на B2B-endpoint’ах.) |
| 503 | SERVICE_UNAVAILABLE | Downstream-зависимость не работает. Повторите с отступом |
Полный справочник кодов
Полный набор значений code, которые вы можете увидеть (совпадает с
payment-service/pkg/errors/errors.go):
Аутентификация и сессии (401)
INVALID_CREDENTIALS— комбинация username/password или API-ключа отклоненаINVALID_TOKEN— JWT/сессионный токен непарсится или повреждёнTOKEN_EXPIRED— JWT прошёлexpINVALID_OTP— OTP не совпалOTP_EXPIRED— OTP был выпущен раньше окна допускаSESSION_EXPIRED— сессия панели устарелаINVALID_SIGNATURE— несовпадение HMAC-подписи на B2B / webhook-вызовах
Авторизация (403)
FORBIDDEN— аутентифицирован, но роль/scope/граница мерчанта блокирует действиеIP_BLOCKED— IP в списке злоупотреблений
Не найдено (404)
NOT_FOUND— общаяRECORD_NOT_FOUND— строка отсутствует по указанному IDUSER_NOT_FOUND— пользователь не найденSESSION_NOT_FOUND— id сессии панели не распознан
Конфликт (409)
ALREADY_EXISTS— общаяUSER_ALREADY_EXISTS— регистрация наткнулась на unique-constraintSESSION_ALREADY_EXISTS— дубль вставки сессии
Валидация (400)
INVALID_INPUT— общая; смотритеdetailsMISSING_REQUIRED— обязательное поле отсутствовалоINVALID_FORMAT— значение не соответствовало ожидаемому формату (например, UUID, URL, email)INVALID_LENGTH— значение слишком короткое или слишком длинноеINVALID_VALUE— значение вне допустимого enum/диапазонаINVALID_USER_STATUS— пользователь в состоянии, запрещающем действиеINVALID_USER_ROLE— у роли нет права на действиеPAYMENT_METHOD_NOT_SUPPORTED— комбинация provider/asset не включена для мерчантаAMOUNT_BELOW_MINIMUM— сумма заказа ниже минимального порога (заданного на уровне сети или окружения). Полеdetails.floor_usdв ответе несёт настроенный порог (в USD), так что вы можете показать его напрямую; текстmessageтоже его называет.INSUFFICIENT_BALANCE— на кошельке покупателя недостаточно платёжного актива, чтобы покрыть перевод.INSUFFICIENT_GAS— на кошельке покупателя не хватает нативного газа, чтобы отправить перевод.
Платежи / биллинг (402)
INSUFFICIENT_CREDIT— предоплаченный баланс мерчанта не покрывает gas / платформенную комиссию. Пополните через панель и повторите
Rate-limit (429)
TOO_MANY_REQUESTS— rate-limit по IP на gatewayTOO_MANY_ATTEMPTS— повторные неудачные попытки на одном ресурсе (например, OTP) сработали как троттл
Хранилище / инфраструктура (500)
DATABASE_CONNECTION_ERROR— не удалось достучаться до БДDATABASE_QUERY_ERROR— план запроса упал на исполненииDATABASE_TRANSACTION_ERROR— commit/rollback не удалсяREDIS_CONNECTION_ERROR— не удалось достучаться до RedisREDIS_OPERATION_ERROR— команда Redis не удаласьEXTERNAL_SERVICE_ERROR— общий сбой стороннего сервиса (provider не выделен отдельно ниже)TWILIO_SERVICE_ERROR— вызов Twilio SMS / Verify не удалсяSENDGRID_SERVICE_ERROR— отправка письма через SendGrid не удаласьINTERNAL_SERVER_ERROR— неожиданный fall-through; снимите заголовокDateответа иX-RateLimit-*и обратитесь
Доступность (503)
SERVICE_UNAVAILABLE— критичный downstream сообщает о нездоровом состоянии; отступ + повтор
Ошибки валидации (400)
Когда проблема в некорректном теле запроса, details — это массив,
чтобы вы могли сопоставить ошибки с полями:
{
"code": 400,
"message": "invalid_input",
"details": [
{ "field": "items[0].unit_price", "message": "must be a positive decimal string" },
{ "field": "success_url", "message": "must be a valid https URL" }
]
}Ошибки аутентификации (401)
INVALID_SIGNATURE покрывает три разных режима отказа — отличить их
можно только методом исключения:
- Неправильный secret-ключ (перепроверьте env var)
- Расхождение по времени > 5 мин (синхронизируйте NTP)
- Неправильная каноническая строка (чаще всего: забыли
\n-разделители или подписали распарсенное-и-пересериализованное тело, отличающееся от отправленного)
См. Аутентификация для точного алгоритма подписи.
Rate-limit’ы (429)
| Уровень | Лимит |
|---|---|
Каждый маршрут gateway (включая /b2b/v1/*) | По IP на gateway — 500 запросов/мин, общая корзина. Не per-merchant. |
Публичный checkout (/checkout/*) | Более строгая под-корзина — 20 запросов/мин на IP |
X-RateLimit-Limit и X-RateLimit-Remaining отправляются на каждом
rate-limited ответе gateway (не только на успешных). Считайте их живым
бюджетом для вашего IP.
Rate-limited ответы содержат заголовок Retry-After (секунды до сброса
окна) — учитывайте его. В качестве fallback отступайте с jitter — 1с
базы + экспонента до 30с.
Rate-limit’ы могут меняться. Если вы упираетесь в них легитимно (например, согласуете большой исторический диапазон), напишите нам — bulk-endpoint’ы в roadmap.
Недостаточный кредит (402)
INSUFFICIENT_CREDIT (HTTP 402 Payment Required) означает, что
предоплаченный баланс мерчанта не покрывает следующую комиссию gas и
платформы для операции, которую вы пытались выполнить — обычно расчёт
крипто-платежа или выполнение gas-спонсируемого on-chain действия.
Пополните из панели мерчанта (Billing → Add credit) и повторите
операцию; работа в полёте подождёт и автоматически продолжится, как
только баланс восстановится.
Серверные ошибки (5xx)
500 означает, что мы не смогли обработать запрос. Повторите с отступом
— ваш idempotency_key гарантирует, что вы не получите двойное
списание, если оригинальный запрос частично выполнился.
Если повторы не восстанавливаются в течение минуты, покажите покупателю
обобщённое «Платёж временно недоступен» и обратитесь в поддержку с
проблемным endpoint’ом, вашим merchant ID, заголовком Date ответа и
значениями X-RateLimit-*, а также приблизительным временем запроса —
этого достаточно, чтобы мы нашли соответствующий запрос в наших логах.
Ошибки доставки webhook
Доставки webhook — отдельный канал отказов — они не всплывают как ошибки API, потому что вызывает не ваш сервер. Когда доставка возвращает non-2xx (или истекает по времени), она ставится в очередь повторов с экспоненциальным отступом 0с, 1мин, 5мин, 15мин, 1ч, 6ч (всего шесть попыток — то же расписание, что и на Webhooks → Обзор). Полная история по каждой доставке доступна в Developers → Webhooks → [endpoint] → Delivery log в панели. После шестой попытки доставка переходит в dead-letter — панель показывает бейдж «Failed», который можно вручную проиграть заново, когда ваш сервер восстановится.