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.
Formato de erro
Todo erro tem o mesmo formato. Use code no seu código (é estável) e message só para mostrar ou registrar.
{
"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-LimiteX-RateLimit-Remaining; ao passar,429comRetry-After(segundos) eX-RateLimit-Reset. - Criação de links: 1.000 por hora e 5.000 por dia. Ao passar,
429 creation_rate_exceededcomdetails.window(hourouday),details.limitedetails.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.
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);