Idempotência
A rede não colabora. O cliente envia a transferência, perde o sinal antes da resposta e tenta de novo. Sem uma chave de idempotência, ele paga duas vezes e descobre no extrato.
Como usar
Toda chamada que move dinheiro exige o cabeçalho Idempotency-Key, com 8 a 128 caracteres, único por operação. Um UUID v4 serve.
Idempotency-Key: 6f1e2a94-7c3d-4b58-9e21-0a8c5d3f7b62Sem ele, a resposta é 400 antes de qualquer efeito. Isso é deliberado: uma API de pagamento que aceita chamada sem chave de idempotência está oferecendo o toque duplo como recurso.
O que acontece ao repetir
| Situação | Resposta |
|---|---|
| Mesma chave, mesmo corpo, operação já concluída | A resposta original, idêntica, com o cabeçalho Idempotent-Replay: true. O dinheiro saiu uma vez só. |
| Mesma chave, mesmo corpo, operação ainda em curso | 409 IDEMPOTENCY_IN_FLIGHT, com Retry-After. Repita em alguns segundos e receba a resposta da primeira. |
| Mesma chave, corpo diferente | 409 IDEMPOTENCY_KEY_REUSED. Devolver a resposta antiga seria mentir sobre uma operação que não aconteceu; executar de novo seria cobrar duas vezes. |
| A operação falhou com erro do servidor | A chave é liberada. Um 500 pode ter acontecido antes de qualquer efeito, e travar a chave para sempre impediria a tentativa que resolveria o problema. |
O escopo da chave
A chave vale por conta e por rota. A mesma chave usada por dois clientes diferentes são duas operações diferentes, e uma não devolve a resposta da outra. A exceção é a cobrança estática: POST /v1/pix/brcode e POST /v1/pix/qrcodes registram pelo mesmo caminho e dividem o espaço de chaves da conta.
A chave é lembrada por 90 dias a partir da primeira requisição. Repetir depois disso conta como operação nova.
Duas requisições ao mesmo tempo
A chave é reservada antes de a operação rodar. Consultar primeiro e gravar depois deixaria uma janela entre as duas em que ninguém tem a chave: duas requisições simultâneas leriam “não existe” e as duas transfeririam. E é exatamente o toque duplo — concorrente, e não sequencial — que a proteção existe para cobrir.
Repetição na prática
async function transferir(pedidoId: string, chavePix: string, centavos: number) {
// A chave nasce do pedido, e não da tentativa: repetir a mesma
// transferência precisa produzir a mesma chave.
const idempotencia = `pedido-${pedidoId}`
for (let tentativa = 0; tentativa < 3; tentativa++) {
const resposta = await fetch(`${base}/v1/pix/transfers`, {
method: 'POST',
headers: {
'X-API-Key': chave,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencia,
},
body: JSON.stringify({ key: chavePix, amountCents: centavos }),
})
if (resposta.ok) return resposta.json()
const erro = await resposta.json()
// A primeira ainda está rodando: esperar é o certo, e a resposta que vem
// depois é a mesma que teria vindo se tivéssemos esperado desde o início.
if (erro.code === 'IDEMPOTENCY_IN_FLIGHT') {
const espera = Number(resposta.headers.get('Retry-After') ?? 2)
await new Promise((r) => setTimeout(r, espera * 1000))
continue
}
throw new Error(`${erro.code}: ${erro.message}`)
}
throw new Error('a transferência não concluiu a tempo')
}