Idempotência e repetição

Para: Dev — Conferido em 2026-10-18.

Dinheiro não admite "talvez tenha ido". Esta página diz, para cada resposta possível, se você repete e como, e garante que uma repetição nunca cria um segundo depósito ou um segundo saque.

O que é

A regra de ouro:

4xx não se repete (é uma decisão do Dracma). 5xx, 429 e erro de rede se repetem com a MESMA Idempotency-Key até uma resposta conclusiva. Nunca gere chave nova para "tentar de novo" um POST de dinheiro.

Dois mecanismos trabalham juntos:

Mecanismo Quem cria Protege contra
Idempotency-Key (cabeçalho) Você, a cada operação de escrita A mesma requisição executar duas vezes (rede que cai, timeout, retry)
external_id (campo do corpo) Você, a cada depósito ou saque Dois objetos para a mesma operação do seu lado, mesmo com chaves diferentes

Quando usar

Idempotency-Key é obrigatório em todo POST: criar depósito, criar saque, cancel, recall, propor mudança de regras, cotação e todas as rotas /sandbox/*. GET e HEAD não usam chave: são sempre seguros de repetir.

Sem a chave, ou com ela fora do formato, a resposta é 400 validation_failed com errors[0] = {field: "Idempotency-Key", reason: "required"}, antes de qualquer efeito.

Como funciona

A chave

  • Formato: 16 a 128 caracteres [A-Za-z0-9_.:-].
  • Escopo: por operação. A mesma chave vale para um único pedido nessa operação, em qualquer rota.
  • Retenção: a resposta fica guardada por 30 dias. Depois disso a chave pode ser reusada, mas o external_id continua único (409 duplicate_external_id).
  • Pedido inteiro: a chave fica ligada ao método, ao caminho com query e ao corpo cru. Qualquer diferença (um espaço, outra ordem de campos) é um pedido diferente.
Situação Resposta
Mesma chave e mesmo pedido, primeira execução já terminou em 2xx ou 4xx A mesma resposta (status e corpo originais), com o cabeçalho Idempotent-Replayed: true
Mesma chave e pedido diferente (outro corpo, outra rota, outra query) 409 idempotency_key_reused. Não é repetível: é um bug do seu lado
Mesma chave enquanto a primeira ainda executa 409 idempotency_in_progress com Retry-After: 1. Repetível
Primeira execução terminou em 5xx ou 429 A chave é liberada: repetir com a mesma chave executa de novo, sem duplicar
A execução anterior ficou presa (o processo do Dracma caiu no meio) A reserva da chave vale 30 s; depois disso a repetição executa

Três consequências que costumam surpreender:

  1. A resposta repetida é um retrato, não o estado atual. O replay devolve o corpo da primeira execução. Para o estado de agora, faça GET /deposits/{id} ou GET /withdrawals/{id}.
  2. Uma recusa 4xx fica gravada. Repetir a mesma chave devolve a mesma recusa por 30 dias, inclusive um 400 de validação. Para tentar de novo depois de uma recusa (o jogador corrigiu o documento, você corrigiu o corpo) é uma operação nova: external_id novo e chave nova.
  3. O nonce não é a chave. Cada tentativa tem novo timestamp, novo nonce e nova assinatura, e a mesma Idempotency-Key (Autenticação HMAC).

Convenção recomendada: chave igual ao external_id

Em POST /deposits e POST /withdrawals, use o external_id como Idempotency-Key, por exemplo dep_<uuidv7> e wdr_<uuidv7> (o UUIDv7 dá ordem temporal e cabe no formato). Vantagens:

  • a chave sobrevive a um restart do seu processo: ela está no seu banco, junto do registro do depósito;
  • não existe "gerar chave nova sem querer": um external_id novo é uma operação nova e consciente;
  • o suporte acha tudo pelo mesmo ID.

Para ações sobre um objeto (cancel), derive a chave de forma determinística, por exemplo cnl_<external_id>. Grave o external_id e a chave antes de chamar o Dracma (padrão outbox): se o processo morrer no meio, o worker que retoma usa a mesma chave.

external_id

  • Único por operação e por tipo: depósito e saque têm espaços separados. De 1 a 64 caracteres [A-Za-z0-9_.:-].
  • Duplicado: um segundo POST com o mesmo external_id e outra chave volta 409 duplicate_external_id, com o objeto já existente em existing. Use-o para conciliar em vez de criar outro.
  • Liga o webhook ao seu registro: no fetch-back, confira que o external_id devolvido é um que você criou (Receptor de webhooks).

Recuperar um POST perdido

Se o timeout estourou e você desistiu de repetir (por exemplo, o processo reiniciou sem a chave), consulte antes de decidir:

GET /v1/deposits?external_id=<seu external_id>
GET /v1/withdrawals?external_id=<seu external_id>
  • Lista vazia ("data": []): o Dracma não criou o objeto. Você pode repetir o POST com a mesma chave (ou uma nova, se a antiga passou de 30 dias).
  • Um item: o objeto existe. Siga pelo estado dele; não crie outro.

GET /deposits/{id} e GET /withdrawals/{id} aceitam só ids no formato do Dracma (dep_…, wdr_…). Um id fora desse formato volta 404 not_found. A consulta por external_id conta no limite de listagem (Limites e quotas): use-a para recuperação, e o GET por id para o fetch-back de rotina.

Por que isso importa no saque: se o POST /withdrawals deu timeout e você devolve a reserva ao jogador sem saber o resultado, e o saque na verdade foi aceito, o jogador recebe o PIX e o saldo de volta. Enquanto não houver resposta conclusiva, a reserva fica onde está.

Backoff

Backoff exponencial com jitter (por exemplo 1 s, 2 s, 4 s, até 60 s), respeitando o Retry-After quando vier. O Retry-After do 429 é calculado pelo balde de limite; o do 503 é de 1 s (5 s em ações de sandbox). Defina um teto de tentativas e um destino para quando ele estourar (a fila de exceções do seu conector).

Árvore de decisão

flowchart TD A[Gravou external_id + Idempotency-Key no seu banco] --> B[POST assinado] B -->|2xx| C[Grave o objeto; siga por webhook + fetch-back] B -->|Idempotent-Replayed: true| C B -->|4xx conclusivo| D[Encerre: não cria objeto. Saque: devolva a reserva] B -->|409 idempotency_in_progress, 429, 5xx| E[Espere Retry-After ou backoff] B -->|timeout ou erro de rede| E E --> F{Tentativas esgotadas?} F -->|não| B F -->|sim| G[Não decida às cegas: GET ?external_id= e marque para revisão + alerta]

Exemplos

Primeira tentativa que deu timeout, seguida de repetição com a mesma chave. A segunda chamada traz novo nonce e nova assinatura:

POST /v1/withdrawals
Idempotency-Key: wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b
Dracma-Nonce: 0123456789abcdef0123456789abcdef
→ (sem resposta: timeout de 10 s)

POST /v1/withdrawals                         (mesmo corpo, byte a byte)
Idempotency-Key: wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b
Dracma-Nonce: fedcba9876543210fedcba9876543210
→ 201 Created
  Idempotent-Replayed: true                  (a primeira tentativa tinha sido executada)

Se o Idempotent-Replayed: true não vem na segunda resposta, a primeira não chegou a ser executada e esta foi a execução real. Nos dois casos o resultado é o mesmo: um único saque.

Erros e casos de borda

Situação O que acontece O que fazer
Chave com menos de 16 caracteres 400 validation_failed (Idempotency-Key, required) Use uma chave de 16 a 128 caracteres
Mesma chave, corpo reserializado com outra ordem de campos 409 idempotency_key_reused Guarde os bytes do corpo na primeira tentativa e reenvie esses bytes
Duas instâncias do conector enviam a mesma chave ao mesmo tempo Uma executa; a outra recebe 409 idempotency_in_progress Espere o Retry-After e repita
4xx de negócio (422 player_blocked) e repetição com a mesma chave A mesma recusa, por 30 dias Não repita; é uma decisão
429 ou 5xx e repetição com a mesma chave Executa de novo, sem duplicar Repita com backoff
Chave de mais de 30 dias A resposta guardada foi apagada Prefira o external_id (continua único) e consulte por ele
409 duplicate_external_id O objeto já existe Leia existing; não crie outro

Veja também