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_namemudavam 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
EXresolve. 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.