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

# Erros

Toda resposta 4xx/5xx carrega o mesmo envelope JSON:

```json
{
  "code": 400,
  "message": "invalid_input",
  "details": [
    { "field": "items[0].unit_price", "message": "must be a positive decimal string" }
  ]
}
```

- `code` — o **status HTTP numérico** (`400`, `401`, `404`, …).
  Útil para tratamento genérico na camada HTTP, mas para lógica de
  ramificação faça switch em `message` em vez disso — `code` não vai
  desambiguar entre, digamos, `invalid_input` e
  `payment_method_not_supported` (ambos 400).
- `message` — o nome sentinela em **lower-snake-case** (por exemplo,
  `INVALID_INPUT` vira `"invalid_input"`). Estável entre releases —
  faça switch nisso.
- `details` — preenchido em erros de validação. Array de objetos
  `{ field, message }` para que o cliente possa amarrar erros a
  inputs. Omitido caso contrário.

> **Warning:**
>
> Os corpos de erro não incluem trace ID nem timestamp. Para reportar
> um problema, envie ao suporte o header `Date` da resposta, os headers
> `X-RateLimit-*`, o ID do seu lojista, o endpoint e o horário
> aproximado da requisição.

> **Note:**
>
> **Falhas de autenticação usam um formato diferente.** O envelope
> acima é o que a API retorna na maioria dos erros. Requisições
> rejeitadas antes de serem processadas (um `X-Signature`
> ausente/inválido, um `X-Client-ID` desconhecido ou um `X-Timestamp`
> velho numa chamada `/b2b/v1/*`) voltam como
> `{ "error": "...", "message": "..." }`, onde `error` é um slug
> grosseiro (`unauthorized` / `bad_request` / `service_unavailable`)
> e `message` carrega as especificidades. Não há `code` numérico
> nem `details`. Ramifique primeiro pelo status HTTP, depois leia
> `message`. Exemplo (401):
>
> ```json
> { "error": "unauthorized", "message": "invalid signature" }
> ```

## Status HTTP → códigos típicos

| HTTP | Valores típicos de `code` | O que significa |
| --- | --- | --- |
| **400** | `INVALID_INPUT`, `MISSING_REQUIRED`, `INVALID_FORMAT`, `INVALID_LENGTH`, `INVALID_VALUE`, `PAYMENT_METHOD_NOT_SUPPORTED`, `AMOUNT_BELOW_MINIMUM` | Requisição ruim — olhe `details` |
| **401** | `INVALID_CREDENTIALS`, `INVALID_TOKEN`, `TOKEN_EXPIRED`, `INVALID_SIGNATURE` (B2B); `SESSION_EXPIRED` (só dashboard) | Auth falhou — chave ruim, timestamp expirado, assinatura errada. Os códigos de login do dashboard não são relevantes para integrações B2B. |
| **402** | `INSUFFICIENT_CREDIT` | Saldo pré-pago do lojista acabou — recarregue antes de retentar |
| **403** | `FORBIDDEN`, `IP_BLOCKED` | Chave é válida, mas não tem o escopo/permissão de IP para esta chamada |
| **404** | `NOT_FOUND`, `RECORD_NOT_FOUND` | Recurso não existe (ou não existe para este lojista) |
| **409** | `ALREADY_EXISTS` | Replay de idempotência com body diferente, ou a máquina de estados recusou a transição |
| **429** | `TOO_MANY_REQUESTS`, `TOO_MANY_ATTEMPTS` | Rate limit batido; backoff e retente |
| **500** | `INTERNAL_SERVER_ERROR`, `EXTERNAL_SERVICE_ERROR` | Erro nosso; seguro retentar com backoff. |
| **503** | `SERVICE_UNAVAILABLE` | Uma dependência downstream caiu. Retente com backoff |

## Referência completa de códigos

Os valores de `code` que você pode ver:

### Autenticação (401)
- `INVALID_CREDENTIALS` — combinação de chaves de API rejeitada
- `INVALID_TOKEN` — token não-parseável ou adulterado
- `TOKEN_EXPIRED` — token expirado
- `SESSION_EXPIRED` — sessão do dashboard expirou
- `INVALID_SIGNATURE` — assinatura HMAC não bate em chamadas B2B / webhook

### Autorização (403)
- `FORBIDDEN` — autenticado, mas você não tem permissão para realizar esta ação
- `IP_BLOCKED` — requisições deste endereço IP estão bloqueadas

### Não encontrado (404)
- `NOT_FOUND` — genérico
- `RECORD_NOT_FOUND` — linha ausente para o ID dado

### Conflito (409)
- `ALREADY_EXISTS` — genérico

### Validação (400)
- `INVALID_INPUT` — genérico; cheque `details`
- `MISSING_REQUIRED` — um campo obrigatório estava ausente
- `INVALID_FORMAT` — valor não combinou com o formato esperado (por exemplo, UUID, URL, e-mail)
- `INVALID_LENGTH` — valor muito curto ou muito longo
- `INVALID_VALUE` — valor fora do enum/range permitido
- `PAYMENT_METHOD_NOT_SUPPORTED` — combinação provider/ativo não está habilitada para o lojista
- `AMOUNT_BELOW_MINIMUM` — valor do pedido abaixo do piso por rede ou por ambiente. A resposta traz `details.floor_usd` com o piso configurado (em USD), então você pode exibi-lo diretamente; o texto de `message` também o informa.
- `INSUFFICIENT_BALANCE` — a carteira do comprador não tem saldo suficiente do ativo de pagamento para cobrir a transferência.
- `INSUFFICIENT_GAS` — a carteira do comprador não tem gas nativo suficiente para transmitir a transferência.

### Pagamento / faturamento (402)
- `INSUFFICIENT_CREDIT` — saldo pré-pago do lojista não consegue cobrir a taxa de rede (gas) / taxa da plataforma. Recarregue pelo dashboard, depois retente

### Rate limiting (429)
- `TOO_MANY_REQUESTS` — rate limit de IP excedido
- `TOO_MANY_ATTEMPTS` — tentativas falhadas repetidas no mesmo recurso (por exemplo, OTP) dispararam um throttle

### Erros de servidor (500)
- `EXTERNAL_SERVICE_ERROR` — um provedor terceiro falhou
- `INTERNAL_SERVER_ERROR` — erro inesperado; fale com o suporte informando o header `Date` da resposta e os headers `X-RateLimit-*`

### Disponibilidade (503)
- `SERVICE_UNAVAILABLE` — o serviço está temporariamente indisponível; retente com backoff

## Erros de validação (400)

Quando o problema é um body de requisição malformado, `details` é
um array para que você possa mapear erros de volta para campos:

```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" }
  ]
}
```

## Erros de auth (401)

`INVALID_SIGNATURE` tem três causas comuns:

- Chave secreta errada (re-cheque a env var)
- Defasagem de timestamp > 5 min (sincronize NTP)
- String canônica errada (mais comum: esqueceu os separadores `\n`
  ou assinou um body parseado-e-re-stringificado que difere do que
  você mandou)

Veja [Autenticação](https://docs.infraio.xyz/pt-BR/api-reference/authentication) para o
algoritmo exato de assinatura.

## Rate limits (429)

| Superfície | Limite |
| --- | --- |
| Todas as rotas da API (incl. `/b2b/v1/*`) | Por endereço IP — **500 req/min**, bucket compartilhado. Não por lojista. |
| Checkout público (`/checkout/*`) | Sub-bucket mais estrito — **20 req/min por IP** |

`X-RateLimit-Limit` e `X-RateLimit-Remaining` são incluídos em
toda resposta com rate limit, não só nas de sucesso. Trate como o orçamento vivo para o seu IP.

Respostas com rate-limit incluem um header `Retry-After` (segundos
até a janela resetar) — honre-o. Como fallback, backoff com jitter —
1s base + exponencial até 30s.

> **Note:**
>
> Os rate limits podem mudar. Se você os atinge com tráfego legítimo
> (por exemplo, reconciliando uma faixa histórica grande), fale com o
> suporte.

## Crédito insuficiente (402)

`INSUFFICIENT_CREDIT` (HTTP **402 Payment Required**) significa que
o seu saldo pré-pago não consegue cobrir a taxa de rede (gas) e a
taxa da plataforma para a operação que você tentou, tipicamente
liquidar um pagamento cripto ou uma ação on-chain cuja taxa de rede
é patrocinada. Recarregue pelo dashboard do lojista
(**Billing → Add credit**), depois retente a operação.
Operações já em andamento continuam automaticamente depois que o
saldo é recarregado.

## Erros de servidor (5xx)

Um 500 significa que a gente não conseguiu processar a requisição.
Retente com backoff — a sua `idempotency_key` garante que você não
vai cobrar em dobro se a requisição original parcialmente teve
sucesso.

Se as retentativas não recuperam em um minuto, exiba um genérico
"pagamento temporariamente indisponível" para o comprador e fale
com o suporte com o endpoint que falhou, o ID do seu lojista, o
header `Date` da resposta e os valores `X-RateLimit-*`, e o horário
aproximado da requisição — isso é o suficiente para a gente
localizar a requisição.

## Erros de entrega de webhook

Entregas de webhook são um canal de falha separado — não aparecem
como erros de API porque não é o seu servidor que está chamando.
Quando uma entrega retorna não-2xx (ou dá timeout), ela é
retentada com backoff exponencial em 0s, 1min, 5min,
15min, 1h, 6h (seis tentativas no total, o mesmo cronograma de
[Webhooks → Visão geral](https://docs.infraio.xyz/pt-BR/webhooks/overview)). O histórico
completo por entrega aparece em **Developers → Webhooks →
[endpoint] → Delivery log** no dashboard. Depois da sexta tentativa
a entrega é marcada como "Failed" no dashboard, e você pode
reenviá-la manualmente assim que o seu servidor estiver saudável.
