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

HTTPcodeQuando
400invalid_requestCampo inválido; details.fields diz qual (ex.: items.2.url)
400invalid_urlDestino não é uma URL http/https válida
401unauthorizedToken ausente, errado ou revogado
403forbidden_scopeA chave não tem o escopo da rota
403email_not_verifiedA conta ainda não confirmou o e-mail
403ip_not_allowedA chave está restrita a outros IPs
404not_foundRecurso não existe ou é de outra conta
409slug_takenO código já está em uso no domínio
409idempotency_conflictMesma Idempotency-Key com corpo diferente
409link_blockedTentativa de reativar link bloqueado pela moderação
422url_blockedDestino inseguro (Google Web Risk ou lista de bloqueio)
422custom_slug_requires_own_domainslug enviado no domínio compartilhado
422domain_not_verifiedDomínio ainda não verificado
429rate_limitedPassou do limite de requisições por minuto
429creation_rate_exceededPassou do teto de criação (hora ou dia)
5xxinternal_errorErro 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.

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);