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-Keyaté uma resposta conclusiva. Nunca gere chave nova para "tentar de novo" umPOSTde 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_idcontinua ú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:
- 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}ouGET /withdrawals/{id}. - Uma recusa
4xxfica gravada. Repetir a mesma chave devolve a mesma recusa por 30 dias, inclusive um400de validação. Para tentar de novo depois de uma recusa (o jogador corrigiu o documento, você corrigiu o corpo) é uma operação nova:external_idnovo e chave nova. - 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_idnovo é 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
POSTcom o mesmoexternal_ide outra chave volta409 duplicate_external_id, com o objeto já existente emexisting. Use-o para conciliar em vez de criar outro. - Liga o webhook ao seu registro: no fetch-back, confira que o
external_iddevolvido é 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 oPOSTcom 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¶
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 |