# Erros, limites e idempotência

> Formato de erro da API do curtei, códigos HTTP, rate limit, teto de criação, Idempotency-Key e paginação por cursor.

Fonte: https://curtei.com/docs/api/erros


## Formato de erro

Todo erro tem o mesmo formato. Use `code` no seu código (é estável) e `message` só para mostrar ou registrar.

```json
{
  "error": {
    "code": "url_blocked",
    "message": "A URL de destino foi classificada como insegura.",
    "details": {
      "reason": "phishing"
    },
    "request_id": "req_01JC..."
  }
}
```

Toda resposta traz `X-Request-Id`. Mande esse valor ao suporte quando algo der errado.

## Códigos

| HTTP | code | Quando |
| --- | --- | --- |
| 400 | `invalid_request` | Campo inválido; `details.fields` diz qual (ex.: `items.2.url`) |
| 400 | `invalid_url` | Destino não é uma URL http/https válida |
| 401 | `unauthorized` | Token ausente, errado ou revogado |
| 403 | `forbidden_scope` | A chave não tem o escopo da rota |
| 403 | `email_not_verified` | A conta ainda não confirmou o e-mail |
| 403 | `ip_not_allowed` | A chave está restrita a outros IPs |
| 404 | `not_found` | Recurso não existe ou é de outra conta |
| 409 | `slug_taken` | O código já está em uso no domínio |
| 409 | `idempotency_conflict` | Mesma `Idempotency-Key` com corpo diferente |
| 409 | `link_blocked` | Tentativa de reativar link bloqueado pela moderação |
| 422 | `url_blocked` | Destino inseguro (Google Web Risk ou lista de bloqueio) |
| 422 | `custom_slug_requires_own_domain` | `slug` enviado no domínio compartilhado |
| 422 | `domain_not_verified` | Domínio ainda não verificado |
| 429 | `rate_limited` | Passou do limite de requisições por minuto |
| 429 | `creation_rate_exceeded` | Passou do teto de criação (hora ou dia) |
| 5xx | `internal_error` | Erro do nosso lado; tente de novo com a mesma `Idempotency-Key` |

## Limites

- Requisições: 120 por minuto por conta. Toda resposta traz `X-RateLimit-Limit` e `X-RateLimit-Remaining`; ao passar, `429` com `Retry-After` (segundos) e `X-RateLimit-Reset`.
- Criação de links: 1.000 por hora e 5.000 por dia. Ao passar, `429 creation_rate_exceeded` com `details.window` (`hour` ou `day`), `details.limit` e `details.retry_after`.
- Não existe cota mensal de links nem plano pago.

## Idempotência

Mande `Idempotency-Key: <uuid>` em todo `POST`. Se a mesma chave chegar de novo em 24 h, a API devolve a resposta original, com `Idempotent-Replayed: true`, sem criar outro link. Assim um retry depois de timeout não duplica nada.

## Paginação

Listas devolvem `{ "data": [...], "next_cursor": "..." }`. Para a próxima página, repita a chamada com `cursor=<next_cursor>`; quando `next_cursor` vier `null`, acabou. `limit` vai até 100.

```js
let cursor = null;
do {
  const url = new URL("https://api.curtei.com/v1/links");
  url.searchParams.set("limit", "100");
  if (cursor) url.searchParams.set("cursor", cursor);
  const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.CURTEI_TOKEN}` } });
  const page = await res.json();
  for (const link of page.data) console.log(link.short_url, link.total_clicks);
  cursor = page.next_cursor;
} while (cursor);
```
