# Erros

Erros vêm em JSON com status HTTP. Quando há um código estável, ele está em `codigo`: trate pelo código, nunca pelo texto da mensagem.

## Status

| Status | Quando | O que fazer |
| --- | --- | --- |
| `401` | Chave ausente, inválida ou revogada. | Confira o header `X-Api-Key` e se a chave está ativa. |
| `404` | CNPJ não encontrado, rota inexistente ou método diferente de `GET`. | Confira CNPJ e rota. Um CNPJ fora da base não quer dizer que a empresa não existe. |
| `409` | Busca ampla que pede confirmação, ou cursor de uma publicação anterior. | Veja o `codigo`. |
| `422` | Parâmetro inválido ou recorte fora da cobertura publicada. | Corrija o parâmetro indicado. |
| `429` | Cota diária esgotada. | Pare até a virada do dia; não repita em laço. |
| `503` | Base em carga, conjunto sem publicação completa ou muitas consultas pesadas ao mesmo tempo. | Veja o `codigo`. Com `Retry-After`, espere esses segundos; nos demais, consulte `/v1/meta` e tente de novo com intervalo crescente. |
| `504` | Busca confirmada interrompida após 60 segundos. | Refine os filtros. |

Rotas inexistentes e métodos diferentes de `GET` respondem `404` em texto puro. Sem chave, uma rota inexistente responde `401`: a chave é verificada antes da rota.

## Formatos do corpo

Um cliente robusto trata os três:

Mensagem simples:

```json
{"detail": "limit deve estar entre 1 e 100"}
```

Com código:

```json
{
  "detail": {
    "codigo": "recorte_nao_coberto",
    "mensagem": "UF ou modalidade fora da janela PNCP publicada."
  }
}
```

Operacional:

```json
{
  "erro": true,
  "codigo": "base_indisponivel",
  "mensagem": "Base em carga inicial ou indisponível. Tente novamente em alguns minutos."
}
```

Python:

```python
def codigo_do_erro(corpo: dict) -> str | None:
    detail = corpo.get("detail")
    if isinstance(detail, dict):
        return detail.get("codigo")
    return corpo.get("codigo")
```

JavaScript:

```javascript
function codigoDoErro(corpo) {
  const detail = corpo?.detail;
  if (detail && typeof detail === "object") return detail.codigo ?? null;
  return corpo?.codigo ?? null;
}
```

## Códigos

| Código | Status | Significado e ação |
| --- | --- | --- |
| `busca_ampla_requer_confirmacao` | 409 | A busca varreria boa parte da base. Traz `custo_estimado` e `linhas_estimadas`. Refine ou repita com `confirmar_varredura=true`. |
| `busca_demorada` | 409, 504 | Passou do tempo limite (12 s, ou 60 s se confirmada). Com `pode_tentar_com_aprovacao: true`, repita com `confirmar_varredura=true`; senão, refine. |
| `municipio_uf_incompativel` | 422 | O município não existe na UF. Traz `ufs_validas` ou `sugestoes` de grafia. |
| `recorte_nao_coberto` | 422 | UF ou modalidade fora da janela PNCP publicada. Veja `meta.ufs` e `meta.modalidades`. |
| `geracao_oportunidades_expirada` | 409 | Nova publicação de oportunidades durante a paginação. Recomece sem cursor. |
| `geracao_despesas_expirada` | 409 | Nova publicação de despesas durante a paginação. Recomece sem cursor. |
| `oportunidades_indisponiveis` | 503 | Não há janela PNCP completa publicada. |
| `despesas_indisponiveis` | 503 | Não há janela de despesas completa publicada. |
| `api_sobrecarregada` | 503 | Há muitas consultas do mesmo tipo em andamento (buscas textuais, totais, páginas profundas, despesas). Vem com `Retry-After`: espere e repita; diminua o paralelismo do cliente. |
| `consulta_demorada` | 503 | A consulta passou do tempo limite e foi interrompida para proteger a base. Refine os filtros ou tente de novo. |
| `base_indisponivel` | 503 | Base em carga ou indisponível. Tente de novo em alguns minutos. |
| `base_carregando` | 503 | `/v1/meta`: o catálogo ainda está carregando. |
