Receptor passo a passo

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

O receptor é o seu endpoint de webhook. Ele precisa verificar a assinatura, deduplicar, confirmar o estado na API e só então mover dinheiro, sempre de forma idempotente. Esta página traz a ordem exata dos 13 passos, o que cada resposta HTTP provoca no Dracma e o pseudocódigo de referência.

O que é

A ordem dos passos é a do operador de referência que a suíte de conformidade usa como oráculo (Suíte de conformidade). Seguir a ordem importa: a verificação barata e a de segurança vêm antes do trabalho caro (consulta à API e efeito no livro).

flowchart TD A[Chega o POST] --> B{1-4: corpo lido,<br/>headers, janela, timestamp} B -- falha --> R401[401 ou 400] B -- ok --> C{5: algum v1 confere<br/>com algum segredo ativo?} C -- não --> R401 C -- sim --> D{6-7: JSON válido e<br/>Event-Id igual ao id?} D -- não --> R400[400] D -- sim --> E{8: já processado?} E -- sim --> R200a[200 sem efeito] E -- não --> F[9: fetch-back na API] F --> G{erro de rede ou 5xx?} G -- sim --> R503[503: o Dracma reentrega] G -- não --> H{10: coerente com o que<br/>você registrou?} H -- não --> R200b[Alerta, sem crédito, 200] H -- sim --> I{11: versão maior que<br/>a aplicada?} I -- não --> R200a I -- sim --> J[12: aplica o efeito<br/>idempotente] J --> K[13: marca visto e responde 200]

Quando usar

Use esta página como especificação ao escrever ou revisar o receptor, e como roteiro de teste: cada linha da tabela de passos é um caso a cobrir. Os vetores de assinatura cobrem os passos 1 a 5.

Como funciona

Os 13 passos, nesta ordem

# Passo Se falhar
1 Leia o corpo cru, com um limite de tamanho folgado (o operador de referência lê até 64 KiB; sugestão: 256 KiB). 400
2 Confira se os headers Dracma-Timestamp e Dracma-Signature estão presentes. Interprete Dracma-Signature como pares chave=valor separados por vírgula: exatamente um t= e pelo menos um v1=. Ignore chaves desconhecidas, que podem ser versões futuras. 401
3 Janela: abs(agora − t) ≤ 300 s. O relógio do seu servidor precisa estar sincronizado por NTP. 401
4 O valor de Dracma-Timestamp tem de ser igual ao t=. 401
5 Assinatura: aceite se qualquer v1 bater com qualquer segredo de webhook ativo. Use comparação em tempo constante e percorra todos os pares, sem sair do laço no primeiro acerto. 401
6 Interprete o JSON. 400
7 Dracma-Event-Id tem de ser igual ao id do corpo. 400
8 Deduplicação: se o Dracma-Event-Id já foi processado com sucesso, responda 200 sem efeito. Guarde os ids por no mínimo 7 dias. 200
9 Fetch-back: para eventos de depósito ou saque, faça GET /v1/deposits/{object_id} ou GET /v1/withdrawals/{object_id} com a sua credencial. Erro de rede ou 5xx: responda 503 para o Dracma reentregar. 404: não mova dinheiro, marque o evento como visto e responda 200.
10 Coerência: confira que o external_id foi criado por você e que amount, currency e player_ref são idênticos aos que você registrou. Em divergência: não credite, marque o caso para revisão, dispare um alerta e responda 200.
11 Versão: se version for menor ou igual à última versão que você aplicou para esse objeto, não faça nada e responda 200. 200
12 Aplique o efeito do estado devolvido pela API (veja a tabela abaixo). O efeito precisa ser idempotente pelo external_id: no máximo um crédito por depósito. Erro interno: 5xx (reentrega)
13 Só depois do sucesso, marque o Dracma-Event-Id como visto e responda 200.

A tabela de respostas na seção seguinte diz o que o Dracma faz com cada código.

O que cada resposta provoca no Dracma

Você responde O Dracma faz
2xx (200 a 299), em até 10 s Dá a entrega como feita (delivered). Nada mais é enviado para esse evento.
3xx Não segue o redirect. Conta como falha e repete.
400, 401, 403, 404, 410, 413, 422 Conta como falha e repete no cronograma, por até 72 h. Nenhum 4xx para a retentativa.
429 Falha, repete no cronograma normal. O Dracma não lê Retry-After.
5xx (por exemplo 503) Falha, repete no cronograma.
Sem resposta em 10 s, conexão recusada, erro de TLS Falha, repete.

O cronograma, a janela de 72 h e a DLQ estão em Retentativas, skipped e DLQ. Pontos de atenção:

  • Responda em até 10 s. Depois disso o Dracma trata a tentativa como falha, mesmo que o seu servidor termine o trabalho. Trabalho pesado vai para uma fila local; só responda 200 depois de gravar o evento de forma durável.
  • O corpo da resposta é ignorado. Só o código de status importa; o Dracma descarta o corpo (lê no máximo 64 KiB).
  • Erro "permanente" não existe para o Dracma. Responder 400 ou 401 não faz o Dracma desistir: o evento volta até 15 ou 16 vezes e vai para a DLQ, com alerta. Para um evento que você não quer tratar, responda 200.
  • Nunca devolva a assinatura esperada nem o segredo na mensagem de erro.

Texto antigo da referência

A descrição do webhook na referência OpenAPI dizia que, em caso de 401, o Dracma "não insiste além da política normal e alerta". Isso induz a erro: o 401 é uma falha como qualquer outra, repetida até a DLQ. O alerta é o e-mail de DLQ ao fim do ciclo.

Fetch-back: por que é obrigatório

Suponha que o seu segredo de webhook vazou. Com ele alguém forja um deposit.paid com assinatura válida. Como você não credita pelo corpo e sim pelo GET feito com a sua credencial, o webhook forjado não passa de uma consulta inútil, e nenhum dinheiro é criado. A suíte de conformidade testa exatamente isto: um webhook forjado com assinatura válida não pode gerar crédito. O fetch-back também resolve a ordem: a consulta devolve sempre o estado mais recente, e o version do objeto ordena as aplicações.

Estado devolvido → ação

O que manda é sempre o estado devolvido pela consulta, não o type do evento. A tabela completa por evento está em Catálogo de eventos; o resumo por estado é:

Estado do objeto Ação
Depósito pending Nenhum dinheiro se move. Mostre o QR Code.
Depósito under_review Nenhum dinheiro se move. O desfecho vem depois.
Depósito paid Credite o jogador pelo valor que você registrou, uma única vez.
Depósito expired, cancelled, failed, returned Terminal, sem crédito.
Depósito refunded, chargeback Débito de depósito creditado: política sua; recomendação de decisão manual com alerta.
Saque under_review, approved, processing Nenhum dinheiro se move; a reserva continua.
Saque paid A reserva vira saída definitiva; guarde psp_reference.
Saque rejected, failed, cancelled Devolva a reserva ao jogador, uma única vez.
Saque returned O banco devolveu depois do paid: política sua; recomendação de decisão manual com alerta.
Qualquer estado ou tipo desconhecido Responda 200, grave o evento, não mova dinheiro e confirme depois.

Idempotência entre "visto" e efeito

A gravação do "evento visto" e o efeito no seu livro precisam ser idempotentes entre si. Há duas formas de garanti-lo: gravar os dois na mesma transação, ou tornar o efeito idempotente pelo external_id (uma restrição de unicidade no lançamento). Assim, um crash entre um passo e outro não credita duas vezes. A segunda forma é a que protege também contra o mesmo evento chegando por webhook e por GET /events.

Tamanho do corpo

O Dracma não impõe um tamanho máximo ao corpo que envia. Na prática, o corpo é o envelope com um depósito ou um saque, alguns KiB, e o operador de referência da conformidade lê até 64 KiB. Configure o limite de leitura do receptor com folga (a sugestão do passo 1 é 256 KiB) e não responda com um erro de tamanho a um evento legítimo: como qualquer não-2xx, ele seria repetido até a DLQ.

Exemplos

Pseudocódigo

handler(req):
  corpo = ler_cru(req, max=256KiB)                 ou 400
  sig   = parse(req.header["Dracma-Signature"])    ou 401   # t= único, >=1 v1
  se |agora - sig.t| > 300:                        401
  se req.header["Dracma-Timestamp"] != sig.t:      401
  ok = falso
  para cada segredo em segredos_webhook_ativos:     # rotação: 1 ou 2
    esperado = HMAC_SHA256(segredo, sig.t + "." + corpo)
    para cada v em sig.v1:
      se tempo_constante_igual(hex_decode(v), esperado): ok = verdadeiro   # sem break
  se não ok:                                       401
  ev = json(corpo)                                 ou 400
  se ev.id != req.header["Dracma-Event-Id"]:       400
  se ja_processado(ev.id):                         200   # dedup >= 7 dias
  se ev.type começa com "deposit." ou "withdrawal.":
    obj, status = GET /v1/{deposits|withdrawals}/{ev.object_id}   # SUA credencial
    se erro ou status >= 500:                      503   # Dracma reentrega
    se status == 404:                              marcar_visto(ev.id); 200
    se não coerente(obj):                          revisar+alertar; marcar_visto(ev.id); 200
    se obj.version <= versao_aplicada(obj.id):     marcar_visto(ev.id); 200
    aplicar_estado(obj)          # idempotente por external_id, na mesma transação de:
  marcar_visto(ev.id)
  200

Há clientes executáveis com verificarWebhook em Go, Node, PHP e shell em Exemplos executáveis.

Tabelas mínimas

-- eventos já aplicados (retenção de pelo menos 7 dias)
CREATE TABLE dracma_eventos_vistos (
  event_id  text PRIMARY KEY,
  visto_em  timestamptz NOT NULL DEFAULT now()
);

-- o efeito financeiro nunca se repete para o mesmo depósito ou saque
CREATE UNIQUE INDEX lancamento_dracma_unico ON lancamentos (external_id, tipo);

Erros e casos de borda

  • O receptor responde 401 a todos os eventos de repente. Quase sempre é rotação de segredo (o receptor só conhece um dos dois) ou relógio fora de sincronia. Veja Problemas comuns.
  • Chegou um evento que você nunca criou. A coerência (passo 10) pega: external_id desconhecido. Não credite, alerte e responda 200.
  • O fetch-back devolve 404. O objeto não existe para a sua chave (pode ser de outra operação, ou um evento forjado). Não mova dinheiro; marque como visto e responda 200.
  • O mesmo evento chega duas vezes ao mesmo tempo. Dois processos podem passar pelo passo 8 juntos. A restrição de unicidade no lançamento (passo 12) é a barreira final.
  • A fila local enche. Responda 2xx só depois de gravar; se a fila não aceita mais, responda 503 e deixe o Dracma repetir.
  • Fuso e relógio. created_at e Dracma-Timestamp estão em UTC.

Veja também