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 é:
- faça
GETdo recurso para lerupdated_atatual; - aplique a mutação com esse valor;
- se vier
409, refaça oGET, 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-*.