> ## Documentation Index
> Fetch the complete documentation index at: https://developers.zouti.com.br/llms.txt
> Use this file to discover all available pages before exploring further.

# Erros

> Formato do erro, o que fazer com cada code e quando repetir a requisição.

A API responde com os status HTTP usuais: `2xx` para sucesso, `4xx` para um problema na requisição
(chave, parâmetro, recurso, limite) e `5xx` para uma falha do lado da Zouti.

Todo erro com corpo JSON tem o mesmo formato:

```json theme={null}
{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_request",
    "message": "O parâmetro limit é inválido.",
    "param": "limit",
    "request_id": "req_8f2kq1x7c3m9v4b6n0zd5a"
  }
}
```

<ResponseField name="error.type" type="string">
  Categoria do erro: `authentication_error`, `permission_error`, `invalid_request_error`,
  `rate_limit_error` ou `api_error`.
</ResponseField>

<ResponseField name="error.code" type="string">
  Motivo do erro, estável e em inglês. **Trate os erros pelo `code`.**
</ResponseField>

<ResponseField name="error.message" type="string">
  Explicação em português, para pessoas. Pode mudar a qualquer momento: não compare o texto.
</ResponseField>

<ResponseField name="error.param" type="string">
  Parâmetro que causou o erro (`limit`, `cursor`, `status`…). Só vem quando o erro é de um
  parâmetro; nos demais casos o campo não aparece.
</ResponseField>

<ResponseField name="error.request_id" type="string">
  Mesmo valor do header `Request-Id`. Informe-o ao suporte.
</ResponseField>

## Códigos

| Status | `type` | `code` | Quando | Repetir? |
| - | - | - | - | - |
| 400 | `invalid_request_error` | `invalid_request` | Parâmetro inválido, nomeado em `param`. | Não |
| 401 | `authentication_error` | `invalid_api_key` | Chave ausente, malformada, inexistente, revogada ou expirada. | Não |
| 403 | `permission_error` | `account_blocked` | A conta dona da chave está bloqueada. | Não |
| 403 | `permission_error` | `insufficient_access` | A chave não tem acesso à operação. | Não |
| 404 | `invalid_request_error` | `resource_missing` | Rota ou recurso inexistente, ou de outra conta. | Não |
| 429 | `rate_limit_error` | `rate_limited` | Limite de requisições excedido. | Sim, depois de `Retry-After` |
| 500 | `api_error` | `internal_error` | Erro inesperado na Zouti. | Sim, com espera |
| 503 | `api_error` | `service_unavailable` | Indisponibilidade temporária. | Sim, com espera |

### `invalid_request`

**400** · `invalid_request_error`

Um parâmetro da requisição é inválido. `param` diz qual: `limit` fora de 1 a 100, data sem fuso,
valor fora da lista em `status`, `payment_method` ou `sort`, ou um `cursor` alterado, de outra
conta ou enviado com filtros ou `sort` diferentes dos da primeira página.

```json theme={null}
{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_request",
    "message": "O parâmetro limit é inválido.",
    "param": "limit",
    "request_id": "req_8f2kq1x7c3m9v4b6n0zd5a"
  }
}
```

**O que fazer:** corrija o parâmetro indicado em `param`. Não repita a mesma requisição. Com
`param: "cursor"`, recomece a listagem sem `cursor`.

<Note>
  Uma requisição em HTTP (sem o S) também recebe `400`, mas com corpo em texto puro
  (`Use HTTPS: a API pública da Zouti não aceita HTTP.`) e sem redirecionamento. Use sempre
  `https://` e revogue a chave que já tenha sido enviada por HTTP.
</Note>

### `invalid_api_key`

**401** · `authentication_error`

A chave está ausente, malformada, não existe, foi revogada ou expirou. A resposta é a mesma em
todos esses casos. A chave enviada na query string (`?api_key=`) também recebe `401`.

```json theme={null}
{
  "error": {
    "type": "authentication_error",
    "code": "invalid_api_key",
    "message": "Chave de API inválida.",
    "request_id": "req_8f2kq1x7c3m9v4b6n0zd5a"
  }
}
```

**O que fazer:** confira o header `Authorization: Bearer <chave>` e envie a chave exatamente como
foi copiada, sem espaço nem quebra de linha. Uma chave recém-criada pode levar cerca de 1 segundo
para valer. Não repita em laço: 30 respostas `401` do mesmo IP em 60 segundos bloqueiam o IP com
`429`.

### `account_blocked`

**403** · `permission_error`

A conta dona da chave está bloqueada.

```json theme={null}
{
  "error": {
    "type": "permission_error",
    "code": "account_blocked",
    "message": "Acesso negado. Esta conta foi bloqueada. Entre em contato com o suporte.",
    "request_id": "req_8f2kq1x7c3m9v4b6n0zd5a"
  }
}
```

**O que fazer:** fale com o suporte da Zouti. Repetir não muda a resposta.

### `insufficient_access`

**403** · `permission_error`

A chave não tem o nível de acesso exigido pela rota.

```json theme={null}
{
  "error": {
    "type": "permission_error",
    "code": "insufficient_access",
    "message": "A chave de API não tem acesso a esta operação.",
    "request_id": "req_8f2kq1x7c3m9v4b6n0zd5a"
  }
}
```

**O que fazer:** use uma chave com acesso à operação. Repetir com a mesma chave não muda a
resposta.

### `resource_missing`

**404** · `invalid_request_error`

O recurso não existe, pertence a outra conta, ou a rota não existe. Um pedido de outra conta
recebe a mesma resposta que um pedido inexistente.

```json theme={null}
{
  "error": {
    "type": "invalid_request_error",
    "code": "resource_missing",
    "message": "Recurso não encontrado.",
    "request_id": "req_8f2kq1x7c3m9v4b6n0zd5a"
  }
}
```

**O que fazer:** confira o identificador (`ord_...`) e o caminho da rota. Não repita.

### `rate_limited`

**429** · `rate_limit_error`

A conta passou do limite de requisições (rajada de 20 e reposição de 10 por segundo, somando
todas as chaves), ou o IP passou do limite de falhas de autenticação (30 respostas `401` em 60
segundos). O header `Retry-After` diz quantos segundos esperar.

```json theme={null}
{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limited",
    "message": "Limite de requisições excedido. Tente novamente em instantes.",
    "request_id": "req_8f2kq1x7c3m9v4b6n0zd5a"
  }
}
```

**O que fazer:** espere os segundos de `Retry-After` e repita. Se acontece com frequência, reduza
as requisições em paralelo. Veja [Limites de requisição](/concepts/rate-limits).

<Warning>
  **429 sem corpo.** Acima de 15.000 requisições em 5 minutos do mesmo IP, a borda responde `429`
  **sem corpo JSON** (sem `error.code`) e com `Retry-After: 300`. Trate o `429` pelo status HTTP,
  não pelo `code`.
</Warning>

### `internal_error`

**500** · `api_error`

Erro inesperado na Zouti.

```json theme={null}
{
  "error": {
    "type": "api_error",
    "code": "internal_error",
    "message": "Erro interno. Se persistir, informe o request_id ao suporte.",
    "request_id": "req_8f2kq1x7c3m9v4b6n0zd5a"
  }
}
```

**O que fazer:** repita com espera crescente. Se persistir, informe o `request_id` ao suporte.

### `service_unavailable`

**503** · `api_error`

Indisponibilidade temporária.

```json theme={null}
{
  "error": {
    "type": "api_error",
    "code": "service_unavailable",
    "message": "Serviço temporariamente indisponível. Tente novamente em instantes.",
    "request_id": "req_8f2kq1x7c3m9v4b6n0zd5a"
  }
}
```

**O que fazer:** repita com espera crescente.

## Autenticação antes da validação

A chave é conferida antes dos parâmetros. Uma requisição com chave errada **e** parâmetro inválido
(por exemplo, `limit=500`) recebe `401` `invalid_api_key`, não `400`. Corrija a chave primeiro.

## Quando repetir

Repita automaticamente só `429`, `500` e `503`. Nunca repita `400`, `401`, `403` ou `404`: a
resposta não muda até a requisição mudar, e `401` repetido bloqueia o IP.

1. Faça no máximo **5 tentativas** no total (a primeira e mais 4).
2. Se a resposta trouxer `Retry-After`, espere esses segundos.
3. Senão, espere `min(30, 0,5 × 2^n)` segundos, com `n` = número de tentativas já feitas − 1, mais
   um jitter aleatório de 0 a 250 ms. Na prática: 0,5 s, 1 s, 2 s e 4 s, cada uma com o jitter.
4. Depois da 5ª tentativa sem sucesso, desista e registre o erro com o `Request-Id`.

O jitter espalha as novas tentativas de vários processos no tempo, para não chegarem todas no
mesmo instante. O código ao lado implementa essa regra.

<RequestExample>
  ```bash cURL theme={null}
  # Repete 429, 500 e 503 em até 5 tentativas; respeita Retry-After quando vem.
  for attempt in 0 1 2 3 4; do
    status=$(curl -sS -o response.json -D headers.txt -w '%{http_code}' \
      -H "Authorization: Bearer $ZOUTI_API_KEY" \
      "https://api.zouti.com.br/v1/orders?limit=10")
    case "$status" in 429|500|503) ;; *) break ;; esac
    [ "$attempt" -eq 4 ] && break
    wait=$(grep -i '^retry-after:' headers.txt | tr -dc '0-9')
    [ -n "$wait" ] || wait=$(awk -v n="$attempt" 'BEGIN { srand(); w = 0.5 * 2 ^ n; if (w > 30) w = 30; printf "%.3f", w + rand() * 0.25 }')
    sleep "$wait"
  done
  echo "HTTP $status"
  cat response.json
  ```

  ```javascript Node.js theme={null}
  // Node.js 18+ (fetch nativo). Repete 429, 500 e 503 em até 5 tentativas.
  const API_KEY = process.env.ZOUTI_API_KEY;
  const BASE_URL = 'https://api.zouti.com.br';
  const RETRYABLE = new Set([429, 500, 503]);
  const MAX_ATTEMPTS = 5;

  class ZoutiError extends Error {
    constructor(status, body, requestId) {
      super(body?.error?.message ?? `HTTP ${status}`);
      this.status = status;
      this.code = body?.error?.code ?? null; // o 429 do limite por IP vem sem corpo
      this.requestId = requestId;
    }
  }

  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

  function waitMs(response, attempt) {
    const retryAfter = Number(response.headers.get('Retry-After'));
    if (retryAfter > 0) return retryAfter * 1000;
    return Math.min(30, 0.5 * 2 ** attempt) * 1000 + Math.random() * 250;
  }

  async function zoutiGet(path) {
    for (let attempt = 0; ; attempt++) {
      const response = await fetch(`${BASE_URL}${path}`, {
        headers: { Authorization: `Bearer ${API_KEY}` },
      });
      if (response.ok) return response.json();

      const body = await response.json().catch(() => null);
      if (!RETRYABLE.has(response.status) || attempt + 1 >= MAX_ATTEMPTS) {
        throw new ZoutiError(response.status, body, response.headers.get('Request-Id'));
      }
      await sleep(waitMs(response, attempt));
    }
  }

  const page = await zoutiGet('/v1/orders?limit=10');
  console.log(`${page.data.length} pedidos; has_more: ${page.has_more}`);
  ```

  ```python Python theme={null}
  # Python 3 com requests. Repete 429, 500 e 503 em até 5 tentativas.
  import os
  import random
  import time

  import requests

  API_KEY = os.environ["ZOUTI_API_KEY"]
  BASE_URL = "https://api.zouti.com.br"
  RETRYABLE = {429, 500, 503}
  MAX_ATTEMPTS = 5


  class ZoutiError(Exception):
      def __init__(self, response):
          try:
              error = response.json().get("error", {})
          except ValueError:
              error = {}  # o 429 do limite por IP vem sem corpo JSON
          super().__init__(error.get("message", f"HTTP {response.status_code}"))
          self.status = response.status_code
          self.code = error.get("code")
          self.request_id = response.headers.get("Request-Id")


  def wait_seconds(response, attempt):
      retry_after = response.headers.get("Retry-After", "")
      if retry_after.isdigit() and int(retry_after) > 0:
          return int(retry_after)
      return min(30, 0.5 * 2**attempt) + random.uniform(0, 0.25)


  def zouti_get(path, params=None):
      for attempt in range(MAX_ATTEMPTS):
          response = requests.get(
              f"{BASE_URL}{path}",
              params=params,
              headers={"Authorization": f"Bearer {API_KEY}"},
              timeout=30,
          )
          if response.ok:
              return response.json()
          if response.status_code not in RETRYABLE or attempt + 1 == MAX_ATTEMPTS:
              raise ZoutiError(response)
          time.sleep(wait_seconds(response, attempt))


  page = zouti_get("/v1/orders", {"limit": 10})
  print(f"{len(page['data'])} pedidos; has_more: {page['has_more']}")
  ```

  ```php PHP theme={null}
  <?php
  // PHP 7.4+ com a extensão curl. Repete 429, 500 e 503 em até 5 tentativas.
  const BASE_URL = 'https://api.zouti.com.br';
  const RETRYABLE = [429, 500, 503];
  const MAX_ATTEMPTS = 5;

  function zouti_get(string $path): array
  {
      $apiKey = getenv('ZOUTI_API_KEY');
      for ($attempt = 0; ; $attempt++) {
          $headers = [];
          $ch = curl_init(BASE_URL . $path);
          curl_setopt_array($ch, [
              CURLOPT_RETURNTRANSFER => true,
              CURLOPT_TIMEOUT => 30,
              CURLOPT_HTTPHEADER => ["Authorization: Bearer {$apiKey}"],
              CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$headers) {
                  $parts = explode(':', $line, 2);
                  if (count($parts) === 2) {
                      $headers[strtolower(trim($parts[0]))] = trim($parts[1]);
                  }
                  return strlen($line);
              },
          ]);
          $body = curl_exec($ch);
          if ($body === false) {
              throw new RuntimeException(curl_error($ch));
          }
          $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

          if ($status >= 200 && $status < 300) {
              return json_decode($body, true);
          }
          if (!in_array($status, RETRYABLE, true) || $attempt + 1 >= MAX_ATTEMPTS) {
              $error = json_decode($body, true)['error'] ?? []; // o 429 do limite por IP vem sem corpo
              throw new RuntimeException(sprintf(
                  'HTTP %d %s (Request-Id %s)',
                  $status,
                  $error['code'] ?? '',
                  $headers['request-id'] ?? '-'
              ));
          }
          $retryAfter = (int) ($headers['retry-after'] ?? 0);
          $wait = $retryAfter > 0 ? $retryAfter : min(30, 0.5 * 2 ** $attempt) + mt_rand(0, 250) / 1000;
          usleep((int) ($wait * 1000000));
      }
  }

  $page = zouti_get('/v1/orders?limit=10');
  printf("%d pedidos; has_more: %s\n", count($page['data']), $page['has_more'] ? 'true' : 'false');
  ```
</RequestExample>

<ResponseExample>
  ```json 400 theme={null}
  {
    "error": {
      "type": "invalid_request_error",
      "code": "invalid_request",
      "message": "O parâmetro limit é inválido.",
      "param": "limit",
      "request_id": "req_8f2kq1x7c3m9v4b6n0zd5a"
    }
  }
  ```

  ```json 429 theme={null}
  {
    "error": {
      "type": "rate_limit_error",
      "code": "rate_limited",
      "message": "Limite de requisições excedido. Tente novamente em instantes.",
      "request_id": "req_8f2kq1x7c3m9v4b6n0zd5a"
    }
  }
  ```
</ResponseExample>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.