Uso

Paginação, idempotência e concorrência

Paginação por cursor

Listagens (GET) usam cursor opaco, never OFFSET:

GET /v1/contacts?limit=50
GET /v1/contacts?limit=50&cursor=eyJhdCI6IjIwMjYtMDgtMTJUMTg6MDA6MDBaIiwiaWQiOiIwYzEuLi4ifQ==

Parâmetros:

  • limit: inteiro de 1 a 100. Padrão 50.
  • cursor: string opaca. Pode estar codificada em base64url. Nunca decodifique, compare ou construa do lado do cliente.

Resposta:

{
  "data": [ /* itens da página */ ],
  "page": {
    "limit": 50,
    "has_more": true,
    "next_cursor": "eyJhdCI6..."
  }
}

Quando has_more = false, next_cursor é null. O cursor codifica (updated_at, id) ou (created_at, id), conforme o recurso, garantindo ordem estável mesmo com timestamps iguais.

A API não calcula COUNT(*) total por padrão. Se precisar saber o total, faça iteração com cursor.

Rate limit

Cada chave tem limites por minuto:

  • 120 leituras/minuto em rotas GET;
  • 30 escritas/minuto em rotas mutação (POST/PATCH/PUT/DELETE);
  • 10 operações de gestão/minuto (criar, rotacionar, revogar chave).

Ao estourar, o status é 429 rate_limit_exceeded com os headers:

Retry-After: 12
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1734134520

Retry-After é em segundos. O identificador do rate limit é o UUID da credencial (ou do usuário, no caso da gestão de chaves), nunca o IP bruto.

Idempotência em mutações

Todas as rotas POST/PATCH/PUT/DELETE exigem o header:

Idempotency-Key: 8-128 caracteres de [A-Za-z0-9._:-]

Regras:

  • Mesma Idempotency-Key + mesmo hash do corpo = resposta replay: devolve exatamente o status e corpo da primeira execução, sem reexecutar a mutação.
  • Mesma Idempotency-Key + corpo diferente = 409 idempotency_conflict.
  • Sem o header em mutação = 400 invalid_request.
  • A chave expira em 24 horas.
  • O corpo idempotente é armazenado em até 2 KiB.

Recomendação de cliente: gere um UUIDv4 por tentativa lógica (não por retry HTTP). Reaproveite a mesma chave quando estiver retentando a mesma intenção após um 5xx/timeout.

Concorrência otimista

Recursos com updated_at exigem expected_updated_at em mutações (PATCH/PUT/DELETE):

PATCH /v1/contacts/0c1d...
Content-Type: application/json
Idempotency-Key: 6f4d2a...

{
  "expected_updated_at": "2026-08-12T18:00:00.000Z",
  "notes": "Texto novo"
}

Se expected_updated_at divergir do estado atual do banco: 409 version_conflict. O fluxo correto é:

  1. faça GET do recurso para ler updated_at atual;
  2. aplique a mutação com esse valor;
  3. se vier 409, refaça o GET, reconcilie a mudança, tente de novo.

Isso evita perda silenciosa de atualização quando dois clientes editam o mesmo recurso ao mesmo tempo.

Limites de payload

  • JSON body máximo: 256 KiB.
  • Strings têm limites por campo (ex.: nome 1-80, anotação até 4.000, descrição de agendamento até 4.000).
  • Datas em ISO 8601 UTC com offset.
  • Dinheiro sempre como string decimal ("1250.00"), nunca número, para evitar erro de ponto flutuante.

Headers de toda resposta

X-Request-Id: 7b1f9c3a-...     # UUID; você pode enviar o seu ( válido) ou deixar a API gerar
Content-Type: application/json; charset=utf-8
Cache-Control: no-store
X-Content-Type-Options: nosniff

Mutação com rate limit ativo também recebe os headers X-RateLimit-*.