Rate Limits, Paginação e Idempotência na API de Assinatura

Uma integração que funciona no ambiente de testes nem sempre sobrevive ao volume de produção. Picos de tráfego esbarram em rate limits, retentativas mal feitas geram cobranças e assinaturas duplicadas, e a listagem de milhares de transações se torna lenta e inconsistente. Este guia técnico reúne os três pilares que separam um protótipo de uma integração robusta com uma API de assinatura digital: rate limits, idempotência e paginação.

Se você já domina os fundamentos descritos no nosso guia da API de assinatura digital e quer levar sua integração para produção, este artigo entra no detalhe operacional que faz a diferença em escala. Vamos cobrir o tratamento de erros 429, o header Retry-After, backoff exponencial com jitter, idempotency keys para reenviar POSTs com segurança e paginação por cursor para varrer grandes coleções.

O conteúdo é agnóstico de linguagem — os exemplos usam cURL e pseudocódigo em JavaScript/Python —, mas se aplica diretamente a quem usa nossos SDKs oficiais ou chamadas REST puras. Para um panorama do tratamento de falhas em geral, vale combinar esta leitura com o artigo sobre códigos de erro e tratamento de falhas na API.

O que são rate limits e por que eles existem

Rate limit é o teto de operações que um cliente pode realizar dentro de uma janela de tempo. Esses limites protegem a plataforma contra abuso, garantem qualidade de serviço para todos os clientes (evitando que um único integrador monopolize a capacidade) e funcionam como uma camada de defesa contra ataques de negação de serviço.

No SignDocs, o mecanismo principal não é um teto de requisições por segundo, e sim cotas de negócio por tenant, com janelas diária e mensal, contadas por tipo de operação — criação de transações e sessões de assinatura têm sua cota; a verificação de PDFs tem cota própria. Os valores são dimensionados no plano sob medida contratado com o time comercial, com possibilidade de franquia adicional (overage) na janela mensal.

Camadas típicas de limite

Camada Janela Como é dimensionada Observação
Cota de transações/sessões (criação) Diária e mensal, por tenant No plano sob medida, conforme o volume contratado Operações de escrita; proteja retentativas com idempotência
Cota de verificação de PDF (POST /v1/verify/document) Diária e mensal, por tenant Cota própria (padrão: 500/dia, 5.000/mês) Cabeçalhos RateLimit-* acompanham as respostas
Franquia adicional (overage) Mensal Negociada no plano Amortece picos sazonais sem bloquear a operação
Token endpoint (OAuth2) Faça cache do bearer token (expira em 15 min); não peça um novo a cada chamada
Dica de eficiência: O token OAuth2 client-credentials expira em 15 minutos. Solicitar um novo token a cada requisição é um antipadrão comum que rapidamente esgota o limite do endpoint de autenticação. Faça cache do token em memória e só renove perto da expiração. Veja os detalhes no guia de autenticação OAuth2.

Headers de rate limit

APIs bem projetadas expõem o estado da sua cota em headers de resposta, permitindo que você se adapte antes de bater no teto. Os mais comuns são:

  • RateLimit-Limit: o total permitido na janela atual.
  • RateLimit-Remaining: quanto ainda resta na janela.
  • RateLimit-Reset: segundos até a janela reiniciar.
  • Retry-After: enviado junto com o 429, indica quantos segundos esperar antes de tentar de novo.

O SignDocs usa a forma padronizada sem o prefixo X- (draft do IETF adotado pelas APIs modernas). Um cliente bem comportado monitora RateLimit-Remaining e, ao perceber que está chegando perto de zero, reduz o ritmo de envio proativamente em vez de esperar pelo erro.

Tratando o erro 429 com Retry-After e backoff

Quando você ultrapassa o limite, a API responde com 429 Too Many Requests. A resposta correta nunca é repetir a chamada imediatamente — isso só agrava a sobrecarga. O fluxo recomendado é: ler o Retry-After, aguardar pelo menos esse tempo e então retomar com backoff exponencial e jitter.

Anatomia de uma resposta 429

HTTP/1.1 429 Too Many Requests Content-Type: application/problem+json Retry-After: 3600 RateLimit-Limit: 500 RateLimit-Remaining: 0 RateLimit-Reset: 3600 { "type": "https://api.signdocs.com.br/errors/too-many-requests", "title": "Too Many Requests", "status": 429, "detail": "Daily transaction quota exceeded" }

Backoff exponencial com jitter

O backoff exponencial aumenta o intervalo entre tentativas a cada falha consecutiva (1s, 2s, 4s, 8s...). O jitter — um componente aleatório somado ao intervalo — evita que vários clientes que falharam ao mesmo tempo tentem novamente em sincronia, fenômeno conhecido como thundering herd. Quando o 429 traz Retry-After, esse valor é o piso mínimo de espera.

// Cliente HTTP com retry para 429 e 5xx (Node.js) async function requestComRetry(url, options, maxTentativas = 5) { for (let tentativa = 0; tentativa < maxTentativas; tentativa++) { const resp = await fetch(url, options); // Sucesso ou erro não recuperável (4xx exceto 429): retorna direto if (resp.status !== 429 && resp.status < 500) return resp; // 429 ou 5xx: calcular espera const retryAfter = parseInt(resp.headers.get('Retry-After')) || 0; const backoff = Math.pow(2, tentativa); // 1, 2, 4, 8... const jitter = Math.random(); // 0..1s aleatório const esperaSeg = Math.max(retryAfter, backoff) + jitter; console.warn(`Status ${resp.status}. Aguardando ${esperaSeg.toFixed(1)}s...`); await new Promise(r => setTimeout(r, esperaSeg * 1000)); } throw new Error('Número máximo de tentativas excedido'); }
Regra de ouro: Aplique retry automático apenas a erros transitórios — 429 e 5xx (502, 503, 504). Erros 4xx como 400 (requisição malformada), 401 (token inválido) e 422 (validação) não devem ser repetidos: o problema está na própria requisição e retentar só desperdiça cota. Esse mapeamento detalhado está no guia de códigos de erro e tratamento de falhas.

Idempotência: reenviando POSTs com segurança

Aqui mora o problema mais sutil de toda integração de pagamento ou assinatura: o que fazer quando um POST dá timeout e você não sabe se ele foi processado? Se você simplesmente reenviar, corre o risco de criar duas transações, dois envelopes ou duas cobranças para o mesmo evento de negócio. Se não reenviar, pode perder a operação. A solução é a idempotência.

Uma requisição é idempotente quando executá-la várias vezes produz o mesmo efeito de executá-la uma única vez. GET e DELETE já são naturalmente idempotentes; POST não é — e é exatamente por isso que precisamos de um mecanismo explícito.

Como funciona a idempotency key

  1. Seu cliente gera um identificador único — tipicamente um UUID v4 — antes de enviar o POST.
  2. Esse valor vai no header X-Idempotency-Key da requisição.
  3. A API armazena a chave junto com o resultado da primeira execução bem-sucedida.
  4. Se a mesma chave chegar de novo (uma retentativa), a API não cria um novo recurso: ela devolve a resposta original memorizada.

O ponto crucial é gerar a chave uma vez por operação de negócio e reutilizá-la em todas as retentativas daquela operação. Gerar uma chave nova a cada tentativa anula completamente a proteção.

Exemplo: criando uma signing session de forma idempotente

O endpoint POST /v1/signing-sessions da Assinatura Expressa cria uma sessão de assinatura em uma única chamada. Veja como protegê-lo com uma idempotency key via cURL:

# Gere o UUID UMA vez e guarde-o junto à sua operação de negócio IDEM_KEY=$(uuidgen) curl -X POST https://api-hml.signdocs.com.br/v1/signing-sessions \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -H "X-Idempotency-Key: $IDEM_KEY" \ -d '{ "purpose": "DOCUMENT_SIGNATURE", "policy": { "profile": "CLICK_PLUS_OTP" }, "signer": { "name": "Maria Silva", "email": "maria@empresa.com.br", "userExternalId": "usr_12345", "cpf": "12345678901" }, "document": { "content": "JVBERi0...", "filename": "Contrato.pdf" } }' # Se a chamada acima der timeout, REPITA-A com o MESMO $IDEM_KEY. # A API devolve a sessão original em vez de criar uma duplicada.

Repare que usamos o host de homologação api-hml.signdocs.com.br (forma com hífen). É o ambiente correto para testar fluxos de retry sem afetar produção — lembrando que as entidades de homologação têm TTL de 7 dias. Para um passo a passo completo em cURL, consulte o guia integrar assinatura digital com cURL/REST.

Respostas possíveis ao reusar uma chave

Cenário Resposta da API O que fazer
Primeira requisição, bem-sucedida 201 Created com o novo recurso Persistir o ID retornado; pronto
Retentativa com a mesma chave e mesmo corpo 200/201 com o recurso original (memorizado) Tratar como sucesso; nenhum recurso novo foi criado
Mesma chave, corpo diferente 409 Conflict — "Idempotency key already used with a different request body" Erro de lógica: você reusou uma chave para outra operação. Gere uma chave nova
Idempotência não substitui deduplicação de negócio. A idempotency key protege retentativas de curto prazo (timeouts, falhas de rede) dentro da janela de retenção da chave — no SignDocs, 24 horas. Para evitar que dois fluxos de negócio distintos criem o mesmo documento dias depois, a lógica de deduplicação precisa morar na sua aplicação, não na chave.

Paginação: listando transações em escala

Endpoints de listagem — como GET /v1/transactions ou a consulta de eventos — nunca devolvem todos os registros de uma vez. Eles paginam o resultado. Entender o modelo de paginação correto evita varreduras lentas, inconsistentes e custosas em cota.

Offset vs. cursor

Existem dois modelos predominantes:

  • Offset/limit: você pede a página N com ?limit=50&offset=100. Simples, mas degrada em grandes volumes e é instável: se registros forem inseridos durante a varredura, itens podem ser pulados ou repetidos.
  • Cursor (keyset): cada resposta devolve um ponteiro opaco (no SignDocs, nextToken) que aponta para onde a próxima página começa. É estável mesmo com inserções concorrentes e tem desempenho constante — é o modelo da API.
Critério Offset/limit Cursor
Desempenho em grandes volumes Degrada conforme o offset cresce Constante, independente da profundidade
Consistência com inserções Baixa: pula ou duplica itens Alta: o cursor é estável
Pular para uma página arbitrária Possível (ir direto à página 10) Não: navegação sequencial
Recomendação Apenas listas pequenas/estáveis Padrão para transações e eventos

Anatomia de uma resposta paginada por cursor

// GET /v1/transactions?limit=50&status=COMPLETED { "transactions": [ { "transactionId": "01JC9P0R2T4V6X8Z0B2D4F6H8K", "status": "COMPLETED" }, { "transactionId": "01JC9P0R2T4V6X8Z0B2D4F6H8L", "status": "COMPLETED" } // ... até 50 itens (default 25, máximo 100) ], "count": 50, "nextToken": "eyJQSyI6IlRFTkFOVCM..." }

Para buscar a próxima página, repita a chamada passando o token: GET /v1/transactions?limit=50&nextToken=eyJQSyI6.... Quando a resposta vier sem nextToken, você chegou ao fim da coleção. Os filtros aceitos incluem status, userExternalId, documentGroupId, startDate e endDate.

Loop de paginação completo

O padrão correto para varrer uma coleção inteira é um laço que segue o cursor até esgotá-lo. Veja em Python, já combinando paginação com respeito ao rate limit:

import time import requests def listar_todas_transacoes(base_url, token, status=None): """Itera por todas as páginas seguindo o cursor.""" headers = {'Authorization': f'Bearer {token}'} params = {'limit': 100} if status: params['status'] = status next_token = None while True: if next_token: params['nextToken'] = next_token resp = requests.get(f'{base_url}/v1/transactions', headers=headers, params=params) # Respeitar rate limit: se 429, aguardar e repetir if resp.status_code == 429: espera = int(resp.headers.get('Retry-After', 5)) time.sleep(espera) continue resp.raise_for_status() body = resp.json() for txn in body['transactions']: yield txn # entrega item a item (streaming) # Avançar para a próxima página ou encerrar next_token = body.get('nextToken') if not next_token: break # Uso: processa todas as transações concluídas sem carregar tudo na memória for txn in listar_todas_transacoes(BASE_URL, TOKEN, status='COMPLETED'): processar(txn)
Boas práticas de paginação: use yield/streaming para não carregar a coleção inteira na memória; aplique filtros no servidor (?status=COMPLETED, ?startDate=...) em vez de baixar tudo e filtrar localmente; e trate o nextToken como opaco — não o decodifique nem o construa manualmente, apenas repasse o valor recebido. Os SDKs oficiais oferecem auto-paginação (listAutoPaginate) que encapsula esse laço.

Consistência: o que esperar dos dados retornados

APIs distribuídas frequentemente operam com consistência eventual em algumas leituras. Isso significa que, logo após criar uma transação, uma consulta de listagem pode levar alguns instantes para refleti-la. Entender esse comportamento evita bugs sutis.

  • Leitura após escrita: consultar um recurso diretamente pelo seu ID (GET /v1/transactions/{id}) logo após criá-lo tende a ser consistente. Já varrer a listagem esperando vê-lo imediatamente pode falhar por replicação em andamento.
  • Ordenação estável: ao paginar por cursor, a ordem é garantida pela chave de ordenação (normalmente data de criação). Não dependa de ordenação implícita em paginação por offset.
  • Fonte da verdade para eventos: para acompanhar mudanças de estado em tempo real, prefira webhooks de eventos a polling de listagem. Use a listagem paginada como reconciliação periódica, não como mecanismo primário.

Para entender como cada estado de uma transação se conecta ao próximo, e em que ponto cada leitura se torna confiável, vale revisar o fluxo transacional completo da API, que detalha o ciclo de vida de ponta a ponta.

SignDocs: integração robusta por padrão. A API SignDocs entrega cotas dimensionadas por plano sob medida, cabeçalhos RateLimit-*, suporte a X-Idempotency-Key em operações de escrita e paginação por nextToken nos endpoints de listagem. Tudo testável no ambiente de homologação gratuito antes de ir para produção. Fale com nossa equipe para dimensionar as cotas do seu caso.

Checklist de integração resiliente

Antes de promover sua integração para produção, valide que ela atende a estes pontos. Se você está começando agora, o quickstart de 5 minutos cobre o caminho inicial; este checklist é o que torna a integração à prova de escala:

  1. Cache de token: o bearer OAuth2 é reaproveitado e só renovado perto da expiração.
  2. Retry seletivo: retentativas apenas para 429 e 5xx, nunca para 4xx de validação.
  3. Backoff com jitter: intervalo exponencial com componente aleatório, respeitando Retry-After.
  4. Idempotency keys: toda escrita (POST) crítica carrega uma chave única por operação de negócio, reutilizada nas retentativas.
  5. Limite de tentativas: um número máximo de retentativas, com falha controlada (dead-letter/alerta) ao esgotá-las.
  6. Paginação por cursor: varreduras seguem o nextToken até ele deixar de vir na resposta, com streaming.
  7. Filtros no servidor: use parâmetros de filtro em vez de baixar tudo e filtrar localmente.
  8. Monitoramento de cota: logue RateLimit-Remaining e alerte quando ele se aproximar de zero recorrentemente.
  9. Eventos via webhook: status em tempo real por webhook; listagem como reconciliação.

Perguntas Frequentes

O que significa o erro HTTP 429 na API de assinatura?

O código 429 (Too Many Requests) indica que sua aplicação ultrapassou o rate limit permitido em uma janela de tempo. A resposta acompanha o header Retry-After, que informa em quantos segundos você pode tentar novamente, além dos cabeçalhos padronizados RateLimit-Limit, RateLimit-Remaining e RateLimit-Reset. O comportamento correto é parar de enviar requisições, aguardar o intervalo indicado em Retry-After e então retomar com backoff exponencial e jitter, em vez de repetir a chamada imediatamente.

Como uma idempotency key evita assinaturas ou cobranças duplicadas?

Uma idempotency key é um identificador único, gerado pelo cliente (tipicamente um UUID v4), enviado no header X-Idempotency-Key de uma requisição POST. O servidor armazena a chave junto com o resultado da primeira execução. Se a mesma chave chegar novamente — por exemplo, após um timeout em que você não sabe se a transação foi criada — a API retorna a resposta original sem criar um novo recurso. Isso permite reenviar POSTs com segurança, sem o risco de gerar transações, envelopes ou cobranças em duplicidade.

Qual a diferença entre paginação por offset e por cursor?

Na paginação por offset, você pede a página N usando parâmetros como limit e offset (ou page). É simples, mas fica lento e inconsistente em grandes volumes: se novos registros forem inseridos durante a varredura, itens podem ser pulados ou repetidos. Na paginação por cursor, cada resposta devolve um ponteiro opaco (na API SignDocs, o nextToken) que aponta para o ponto exato onde a próxima página começa. O cursor é estável mesmo com inserções concorrentes e tem desempenho constante, sendo a abordagem recomendada para listar transações e eventos em produção.

Por quanto tempo a API guarda uma idempotency key?

No SignDocs, a chave é retida por 24 horas. Durante esse intervalo, repetir uma requisição com a mesma chave e o mesmo corpo retorna o resultado memorizado (e a mesma chave com corpo diferente retorna 409 Conflict). Após a expiração, a chave é descartada e uma nova requisição com aquele mesmo valor seria tratada como inédita. Por isso, idempotency keys são ideais para proteger retentativas de curto prazo (timeouts, falhas de rede), não para deduplicação de longo prazo, que deve ser feita pela lógica de negócio da sua aplicação.

Devo usar backoff fixo ou exponencial ao receber 429 e 5xx?

Use backoff exponencial com jitter. Em backoff fixo, todos os clientes que falharam tendem a tentar novamente no mesmo instante, criando picos sincronizados que sobrecarregam o servidor em recuperação (efeito thundering herd). No backoff exponencial, o intervalo dobra a cada tentativa (1s, 2s, 4s, 8s...) e o jitter — um componente aleatório somado ao intervalo — espalha as retentativas no tempo. Quando a resposta 429 traz Retry-After, respeite esse valor como piso mínimo de espera.

O endpoint público de verificação tem o mesmo rate limit das chamadas autenticadas?

Não. A consulta pública por evidenceId (GET /v1/verify/{evidenceId}) é aberta e protegida contra abuso na borda, enquanto a verificação de PDFs (POST /v1/verify/document) é autenticada e tem cota própria por tenant — por padrão, 500 verificações por dia e 5.000 por mês, com cabeçalhos RateLimit-* na resposta. Para validação programática em alto volume, use o fluxo autenticado e dimensione a cota no plano sob medida.

Construa integrações que aguentam produção

A API SignDocs entrega cotas por plano sob medida, idempotency keys, paginação por cursor e ambiente de homologação gratuito — tudo o que sua integração precisa para escalar com segurança. ICP-Brasil nativo, LGPD-first e suporte em português.

Fale com o time comercial Conheça a plataforma grátis