BETAHubCard beta — feedback bem-vindo.
engenharia12 de maio de 2026·6 min de leitura

Idempotência sem dor na API de emissão de cartões: a chave que sobrevive a retries agressivos

Como projetamos Idempotency-Key na API de emissão de cartões corporativos para sobreviver a 2 milhões de requisições duplicadas no primeiro semestre.

TA
Tiago Andrade
CTO
engenharia
// emit a card with idempotency
POST /v2/cards
{ supplier: "openai",
  limit_cents: 800_000 }

No primeiro semestre de 2026, nossa API de emissão de cartões corporativos processou 2 milhões de requisições duplicadas — e nenhuma delas criou um cartão extra. Esse post é sobre como projetamos a chave de idempotência que sobreviveu a retries agressivos de clientes, agentes de IA com jitter mal calibrado e webhooks que insistiam em reentregas mesmo depois de 2xx.

Por que idempotência importa em emissão de cartões

Emitir um cartão pré-pago não é uma operação inócua. Envolve provisionar BIN range, alocar saldo, registrar no processador, gerar PAN e tokenizar para wallets. Se a operação acontece duas vezes, você tem dois cartões reais, dois saldos bloqueados, e uma reconciliação cansativa.

Quando um cliente roda POST /v2/cards e o response demora 2.4s, é totalmente racional que ele dispare um retry. Em sistemas com fila de eventos, é racional que o handler reentregue. Em agentes de IA com jitter mal calibrado, isso vira tempestade.

A regra é simples: a mesma chave de idempotência tem que produzir exatamente o mesmo recurso, mesmo um ano depois.

A primeira versão (que quebrou)

Nossa primeira tentativa foi a clássica: hash do body como chave. Recebia o request, calculava SHA-256 do payload normalizado, buscava em Redis, devolvia o resultado armazenado.

Quebrou em três cenários:

  • Timestamps no payload: cliente que incluía created_at: now() gerava hash diferente a cada retry. Cartão duplicado.
  • JSON com chaves em ordem diferente: o mesmo client gerando {a: 1, b: 2} e {b: 2, a: 1} produzia hashes diferentes.
  • Encoding de Unicode: caracteres acentuados em metadata.supplier_name mudavam a representação bytes.

A lição: a chave de idempotência tem que ser fornecida pelo cliente, não derivada do payload. O cliente sabe o que é uma operação única. Você não.

O padrão Idempotency-Key

Adotamos o header Idempotency-Key, inspirado em Stripe. A regra do contrato:

POST /v2/cards
Idempotency-Key: emit_openai_2026_05_12_run_3814
Content-Type: application/json

{ "supplier": "openai", "limit_cents": 800000 }

A chave vive 24h em Redis. Cada chave armazena:

  • O request hash (para validar que o body é o mesmo)
  • O status do request (in_flight | succeeded | failed)
  • O response completo (status code + body + headers relevantes)

Comportamentos:

  • Se a chave não existe: marca como in_flight, processa, salva resultado, retorna.
  • Se a chave existe como succeeded: retorna o response cached, sem reprocessar.
  • Se a chave existe como in_flight: bloqueia até 10s aguardando, depois retorna 409.
  • Se a chave existe mas o request hash difere: retorna 422 — você está usando a mesma chave para operações diferentes, isso é bug.

Por que in_flight importa

O cenário que mais nos mordeu não foi retry — foi paralelismo. Cliente disparava o mesmo POST em duas threads simultaneamente. Sem lock, ambos achavam que a chave não existia, ambos criavam cartão.

O in_flight resolve isso com SET NX EX 10 no Redis: a primeira thread vence o lock. As outras veem in_flight e esperam. Quando a primeira termina, escreve o resultado e libera. As outras leem o resultado pronto.

Para os 0.3% dos casos em que o lock expira antes do processamento terminar (processador externo lento), retornamos 409 e logamos. O cliente pode tentar de novo com a mesma chave.

A armadilha do storage

Idempotency-Key parece simples até você precisar:

  • TTL: 24h é o sweet spot. Menos, retries de webhooks falham. Mais, storage explode.
  • Garbage collection: Redis com EX resolve. Não use TTL em DB relacional — vai virar bottleneck.
  • Migração: quando rotacionar a chave de hash, mantenha leitura compatível por 48h.
  • Multi-região: replicação eventual mata idempotência. Use o mesmo Redis primário, ou um lock distribuído.

O endpoint de inspeção

Adicionamos GET /v2/idempotency/:key para o cliente inspecionar. Retorna status atual, request hash, response cached. Útil em debugging quando o cliente acha que mandou X e o servidor recebeu Y.

curl https://api.hubcard.one/v2/idempotency/emit_openai_2026_05_12_run_3814 \
  -H "Authorization: Bearer sk_live_••••"

{
  "key": "emit_openai_2026_05_12_run_3814",
  "status": "succeeded",
  "first_seen_at": "2026-05-12T14:22:09Z",
  "response": { "id": "card_01HX8...", "status": "active" }
}

O resultado, em números

Depois de seis meses com esse desenho:

  • 2.04 milhões de requisições duplicadas detectadas (≈ 14% do volume total).
  • Zero cartões duplicados emitidos por bug de idempotência.
  • Latência adicional: 4ms p99 para lookup no Redis.
  • Falsos positivos (mesmo Idempotency-Key + body diferente): 312 casos, todos bugs de cliente — devolvidos com 422 e mensagem explícita.

Idempotência não é um nice-to-have. É a forma de dizer aos integradores: "retry à vontade. Você nunca vai cobrar duas vezes.". Se você está construindo uma API de cartões pré-pago ou qualquer endpoint que move dinheiro, isso precisa ser commodity.

TA

Tiago Andrade

CTO

Escreve sobre cartões corporativos, API de emissão e gestão de despesas em tempo real.

Falar com o time

Continue lendo

Todos os posts
Idempotência sem dor na API de emissão de cartões: a chave que sobrevive a retries agressivos · HubCard