<!-- Source: https://docs.infraio.xyz/ru/api-reference/errors -->
<!-- Last updated: 2026-10-04 -->

# Ошибки

Каждый ответ 4xx/5xx несёт один и тот же JSON-конверт:

```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** (например,
  `INVALID_INPUT` превращается в `"invalid_input"`). Стабильно между
  релизами — переключайтесь по нему.
- `details` — заполняется при ошибках валидации. Массив объектов
  `{ field, message }`, чтобы клиент мог привязать ошибки к полям
  ввода. В остальных случаях не указывается.

> **Warning:**
>
> Тела ошибок не содержат trace ID и timestamp. Чтобы сообщить о проблеме,
> отправьте в поддержку заголовок `Date` ответа, заголовки `X-RateLimit-*`,
> ваш merchant ID, endpoint и приблизительное время запроса.

> **Note:**
>
> **Ошибки аутентификации используют другую форму.** Конверт выше — то, что
> API возвращает для большинства ошибок. Запросы, отклонённые до обработки
> (отсутствующая/некорректная `X-Signature`, неизвестный `X-Client-ID` или
> устаревший `X-Timestamp` на вызове `/b2b/v1/*`), приходят как
> `{ "error": "...", "message": "..." }`, где `error` — грубый slug
> (`unauthorized` / `bad_request` / `service_unavailable`), а `message`
> несёт конкретику. Числового `code` и `details` нет. Сначала ветвитесь по
> HTTP-статусу, потом читайте `message`. Пример (401):
>
> ```json
> { "error": "unauthorized", "message": "invalid signature" }
> ```

## HTTP-статус → типичные коды

| HTTP | Типичные значения `code` | Что значит |
| --- | --- | --- |
| **400** | `INVALID_INPUT`, `MISSING_REQUIRED`, `INVALID_FORMAT`, `INVALID_LENGTH`, `INVALID_VALUE`, `PAYMENT_METHOD_NOT_SUPPORTED`, `AMOUNT_BELOW_MINIMUM` | Некорректный запрос — смотрите `details` |
| **401** | `INVALID_CREDENTIALS`, `INVALID_TOKEN`, `TOKEN_EXPIRED`, `INVALID_SIGNATURE` (B2B); `SESSION_EXPIRED` (только панель) | Аутентификация не прошла — неверный ключ, истёкший timestamp, неправильная подпись. Коды входа в панель к B2B-интеграциям не относятся. |
| **402** | `INSUFFICIENT_CREDIT` | Предоплаченный баланс мерчанта закончился — пополните перед повтором |
| **403** | `FORBIDDEN`, `IP_BLOCKED` | Ключ валиден, но у него нет scope'а / разрешения по IP для этого вызова |
| **404** | `NOT_FOUND`, `RECORD_NOT_FOUND` | Ресурс не существует (или не существует для этого мерчанта) |
| **409** | `ALREADY_EXISTS` | Idempotency-replay с другим телом, либо state-машина отказала в переходе |
| **429** | `TOO_MANY_REQUESTS`, `TOO_MANY_ATTEMPTS` | Достигнут rate-limit; сделайте отступ и повторите |
| **500** | `INTERNAL_SERVER_ERROR`, `EXTERNAL_SERVICE_ERROR` | Наша вина; безопасно повторить с отступом. |
| **503** | `SERVICE_UNAVAILABLE` | Downstream-зависимость не работает. Повторите с отступом |

## Полный справочник кодов

Значения `code`, которые вы можете увидеть:

### Аутентификация (401)
- `INVALID_CREDENTIALS` — комбинация API-ключа отклонена
- `INVALID_TOKEN` — токен непарсится или повреждён
- `TOKEN_EXPIRED` — срок действия токена истёк
- `SESSION_EXPIRED` — сессия панели истекла
- `INVALID_SIGNATURE` — несовпадение HMAC-подписи на B2B / webhook-вызовах

### Авторизация (403)
- `FORBIDDEN` — аутентификация пройдена, но выполнять это действие вам нельзя
- `IP_BLOCKED` — запросы с этого IP-адреса заблокированы

### Не найдено (404)
- `NOT_FOUND` — общая
- `RECORD_NOT_FOUND` — строка отсутствует по указанному ID

### Конфликт (409)
- `ALREADY_EXISTS` — общая

### Валидация (400)
- `INVALID_INPUT` — общая; смотрите `details`
- `MISSING_REQUIRED` — обязательное поле отсутствовало
- `INVALID_FORMAT` — значение не соответствовало ожидаемому формату (например, UUID, URL, email)
- `INVALID_LENGTH` — значение слишком короткое или слишком длинное
- `INVALID_VALUE` — значение вне допустимого enum/диапазона
- `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
- `TOO_MANY_ATTEMPTS` — повторные неудачные попытки на одном ресурсе (например, OTP) сработали как троттл

### Серверные ошибки (500)
- `EXTERNAL_SERVICE_ERROR` — сбой стороннего провайдера
- `INTERNAL_SERVER_ERROR` — неожиданная ошибка; обратитесь в поддержку, указав заголовок `Date` ответа и заголовки `X-RateLimit-*`

### Доступность (503)
- `SERVICE_UNAVAILABLE` — сервис временно недоступен; повторите с отступом

## Ошибки валидации (400)

Когда проблема в некорректном теле запроса, `details` — это массив,
чтобы вы могли сопоставить ошибки с полями:

```json
{
  "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`-разделители
  или подписали распарсенное-и-пересериализованное тело, отличающееся от
  отправленного)

См. [Аутентификация](https://docs.infraio.xyz/ru/api-reference/authentication) для точного
алгоритма подписи.

## Rate-limit'ы (429)

| Уровень | Лимит |
| --- | --- |
| Все маршруты API (включая `/b2b/v1/*`) | По IP-адресу — **500 запросов/мин**, общая корзина. Не per-merchant. |
| Публичный checkout (`/checkout/*`) | Более строгая под-корзина — **20 запросов/мин на IP** |

`X-RateLimit-Limit` и `X-RateLimit-Remaining` включаются в каждый
rate-limited ответ, а не только в успешные. Считайте их живым бюджетом
для вашего IP.

Rate-limited ответы содержат заголовок `Retry-After` (секунды до сброса
окна) — учитывайте его. В качестве fallback отступайте с jitter — 1с
базы + экспонента до 30с.

> **Note:**
>
> Rate-limit'ы могут меняться. Если вы упираетесь в них при легитимном
> трафике (например, согласуете большой исторический диапазон),
> обратитесь в поддержку.

## Недостаточный кредит (402)

`INSUFFICIENT_CREDIT` (HTTP **402 Payment Required**) означает, что ваш
предоплаченный баланс не покрывает комиссию сети (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 → Обзор](https://docs.infraio.xyz/ru/webhooks/overview)).
Полная история по каждой доставке доступна в **Developers →
Webhooks → [endpoint] → Delivery log** в панели. После шестой попытки
доставка помечается в панели как «Failed», и вы можете вручную
повторить её, когда ваш сервер восстановится.
