Requisição e assinatura¶
Para: Dev — Conferido em 2026-10-18.
Cada webhook é um POST com corpo JSON, três headers Dracma-* e uma assinatura HMAC-SHA256 calculada sobre os bytes exatos do corpo. Esta página descreve a requisição que o seu receptor recebe e o cálculo da assinatura, com vetores de teste que toda implementação precisa reproduzir.
O que é¶
O Dracma envia o webhook como uma requisição POST para a URL cadastrada na sua operação (veja Seu endpoint). Toda tentativa leva:
| Item | Valor |
|---|---|
| Método | POST |
Content-Type |
application/json; charset=utf-8 |
User-Agent |
Dracma-Webhooks/1 |
Dracma-Event-Id |
Id do evento, no formato evt_ seguido de 32 caracteres hexadecimais minúsculos. É igual ao campo id do corpo e é a chave de deduplicação. |
Dracma-Timestamp |
Segundos Unix (UTC) do envio, em decimal. É igual ao t= de Dracma-Signature. |
Dracma-Signature |
t=<timestamp>,v1=<hex>; durante uma rotação de segredo chegam dois v1. |
O User-Agent identifica o emissor para logs e regras de proxy, mas não é autenticação: qualquer cliente pode enviá-lo. Quem autentica é a assinatura.
O Dracma não envia nenhum header de rastreamento (traceparent, tracestate) ao seu receptor. Para pedir ajuda ao suporte, use Dracma-Event-Id e o operation_id do corpo (veja Problemas comuns).
Quando usar¶
Leia esta página ao implementar a verificação do receptor e use os vetores como teste de aceitação antes de apontar a URL para o Dracma. A verificação da assinatura é o primeiro passo do receptor (Receptor passo a passo); ela vem antes de interpretar o JSON.
Como funciona¶
A assinatura¶
v1 = hex_minúsculo( HMAC-SHA256( segredo_de_webhook, "<t>." + corpo_cru ) )
segredo_de_webhooké a string inteira, com o prefixowhsec_, usada como está (como bytes UTF-8) para chave do HMAC. Não remova o prefixo e não decodifique o restante como hexadecimal.<t>é o valor decimal dot=, sem sinal e sem zeros à esquerda, seguido de um ponto (.).corpo_crusão os bytes exatos recebidos. Calcule a assinatura antes de interpretar o JSON e nunca sobre um JSON serializado de novo, porque a reserialização muda os bytes.- O
v1vem em hexadecimal minúsculo, com 64 caracteres.
O segredo de webhook (whsec_…) é diferente do segredo de chamada da API. Guarde os dois no cofre, separados. O formato e o ciclo de vida estão em Segredo e rotação.
O header Dracma-Signature¶
O header é uma lista de pares chave=valor separados por vírgula:
- Exatamente um
t=, em forma canônica: só dígitos, sem sinal (+,-), sem espaços e sem zero à esquerda (o próprio0é válido). O Dracma nunca emite outra forma. - Um
v1=por segredo ativo, o mais novo primeiro. São no máximo dois (veja abaixo).
O Dracma gera um t= novo e uma assinatura nova em cada tentativa de entrega. Por isso uma reentrega de horas depois continua dentro da janela de 300 s: a janela vale para o t= da tentativa, não para a hora do evento.
Hoje o Dracma emite só as chaves t e v1. Quando o header trouxer uma chave que você não conhece, ignore-a: ela pode ser um esquema de assinatura de versão futura. Rejeitar o header por causa de uma chave desconhecida quebraria o seu receptor no dia em que ela aparecer.
Dois v1 durante a rotação¶
Cada operação pode ter até dois segredos de webhook ativos ao mesmo tempo: o mais novo e, por até 7 dias depois de uma rotação, o anterior. Enquanto os dois valem, o header traz dois v1 (o do segredo novo primeiro). O seu receptor aceita se qualquer v1 bater com qualquer segredo que ele conhece. Isso permite trocar o segredo sem perder entrega. O procedimento completo está em Segredo e rotação.
O corpo é idêntico em toda tentativa¶
O Dracma monta o corpo uma vez, quando o evento nasce, grava os bytes e reenvia exatamente os mesmos bytes em toda tentativa e em toda reentrega manual. O que muda de uma tentativa para a outra é só Dracma-Timestamp, o t= e a assinatura. Duas consequências práticas:
- A deduplicação por
Dracma-Event-Idé segura: o mesmo evento tem sempre o mesmoide o mesmo conteúdo. - O
data.objecté o estado do objeto no momento da transição, congelado. Uma reentrega feita horas depois leva o objeto antigo. Por isso o receptor confirma o estado atual com uma consulta à API (o fetch-back), descrito em Receptor passo a passo.
Exemplos¶
Uma requisição completa¶
POST /dracma/webhook HTTP/1.1
Host: pagamentos.operador.example
Content-Type: application/json; charset=utf-8
User-Agent: Dracma-Webhooks/1
Dracma-Event-Id: evt_01J9ZQ5
Dracma-Timestamp: 1790000300
Dracma-Signature: t=1790000300,v1=4f82d5dac571d1a7b4dae9ef852c74cffcb6a4ce76f7f3209d685798e02a229b
{"id":"evt_01J9ZQ5","type":"deposit.paid","created_at":"2026-09-26T15:00:00Z","operation_id":"op_nero_hml","object_id":"dep_01J9ZQ3","object_version":3,"data":{"object":{"id":"dep_01J9ZQ3","external_id":"dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b","status":"paid"}}}
Este é o corpo do vetor de teste normativo: o op_nero_hml é um identificador fictício e os ids evt_01J9ZQ5 e dep_01J9ZQ3 são curtos de propósito. Em produção o operation_id é um UUID, os ids têm 32 caracteres hexadecimais e o data.object traz o depósito ou o saque completo. O formato real está em Envelope e Catálogo de eventos.
Vetores normativos¶
Os vetores de vetores-assinatura.json são públicos e de teste: nunca use esses segredos fora de teste. Toda implementação precisa reproduzi-los byte a byte. Para testar a janela de tempo, fixe "agora" em 1790000300.
Vetor 1: assinatura simples
- Segredo:
whsec_test_vetor_fedcba9876543210 - Mensagem assinada:
1790000300.seguido do corpo cru mostrado acima. v1=4f82d5dac571d1a7b4dae9ef852c74cffcb6a4ce76f7f3209d685798e02a229b- Resultado: aceita.
Vetor 2: rotação (dois v1)
Dracma-Signature: t=1790000300,v1=4f82d5dac571d1a7b4dae9ef852c74cffcb6a4ce76f7f3209d685798e02a229b,v1=f146fd13a7702bb83873e1f2ff96947c854086637cc252e536b18f3dd6569920
- O primeiro
v1usa o segredo novo (whsec_test_vetor_fedcba9876543210). O segundo usa o anterior (whsec_test_vetor_ANTERIOR). - Um receptor que tem só o segredo novo aceita, porque o 1º
v1bate. - Um receptor que ainda tem só o segredo anterior também aceita, porque o 2º
v1bate. É isso que permite rotacionar sem perder nenhuma entrega.
Vetor 3: negativo (DEVE recusar)
- O corpo tem um byte alterado (
"object_version":4no lugar de3), mas o header é a assinatura do vetor 1. - O HMAC correto desse corpo seria
47ab23d3…c77a6995, e ele não bate com4f82d5da…. Resultado: 401. - Um receptor que aceita esse vetor está verificando outra coisa que não o corpo cru (por exemplo, o JSON reserializado ou só alguns campos).
Para conferir o vetor 1 no terminal (o printf não adiciona quebra de linha ao final):
printf '%s' '1790000300.{"id":"evt_01J9ZQ5","type":"deposit.paid","created_at":"2026-09-26T15:00:00Z","operation_id":"op_nero_hml","object_id":"dep_01J9ZQ3","object_version":3,"data":{"object":{"id":"dep_01J9ZQ3","external_id":"dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b","status":"paid"}}}' \
| openssl dgst -sha256 -hmac whsec_test_vetor_fedcba9876543210
A saída termina em …02a229b. Para conferir o vetor 2 com o segredo anterior, troque a chave por whsec_test_vetor_ANTERIOR: o resultado começa com f146fd13.
Verificação em Python¶
O trecho abaixo faz as três primeiras verificações da ordem do receptor (formato do header, janela e assinatura) e reproduz os três vetores. corpo são os bytes crus da requisição, antes de qualquer parse.
import hashlib, hmac, re, time
CANONICO = re.compile(r"^(0|[1-9][0-9]{0,18})$")
def verificar(segredos, ts_header, sig_header, corpo, agora=None):
"""Devolve None se válido, ou o motivo (responda 401). corpo = bytes crus."""
if not ts_header or not sig_header:
return "headers ausentes"
t, v1 = [], []
for parte in sig_header.split(","):
chave, _, valor = parte.strip().partition("=")
if chave == "t":
t.append(valor)
elif chave == "v1":
v1.append(valor)
# chaves desconhecidas: ignore (versões futuras)
if len(t) != 1 or not v1 or not CANONICO.match(t[0]):
return "Dracma-Signature malformado"
if ts_header != t[0]:
return "Dracma-Timestamp difere do t="
if abs((agora if agora is not None else time.time()) - int(t[0])) > 300:
return "fora da janela de 300 s"
ok = False
for segredo in segredos: # 1 ou 2 segredos ativos
esperado = hmac.new(segredo.encode(), t[0].encode() + b"." + corpo, hashlib.sha256).hexdigest()
for recebido in v1:
if hmac.compare_digest(esperado, recebido.lower()):
ok = True # sem sair do laço no primeiro acerto
return None if ok else "nenhum v1 confere"
Há clientes executáveis com verificarWebhook em Go, Node, PHP e shell em Exemplos executáveis.
Erros e casos de borda¶
| Situação | Causa provável | O que fazer |
|---|---|---|
Nenhum v1 confere, mas o segredo parece certo |
O corpo foi lido por um framework que já interpretou e reserializou o JSON, ou o proxy alterou os bytes (compressão, quebra de linha, charset). |
Leia o corpo cru antes de qualquer middleware que interprete JSON. Registre o tamanho em bytes do corpo recebido e confira o hash do corpo antes e depois dos seus middlewares. |
| Funciona com um segredo e falha logo depois de uma rotação | O receptor só conhece o segredo antigo ou só o novo e comparou só o primeiro v1. |
Mantenha a lista de segredos ativos (1 ou 2) e teste todos os pares v1 × segredo. |
Dracma-Timestamp diferente do t= |
Um proxy reescreveu um dos headers, ou o seu código leu o header errado. | Os dois são sempre iguais na origem; reprove com 401. |
| Falha só em reentregas | O relógio do servidor está fora de sincronia. A janela vale para o t= da tentativa, mas depende do seu relógio estar certo. |
Sincronize o relógio por NTP. |
O header traz uma chave além de t e v1 |
Esquema de assinatura de versão futura. | Ignore a chave. Nunca falhe a verificação por causa dela. |
| O vetor 3 foi aceito | Você verifica o JSON reserializado ou só alguns campos. | Verifique os bytes crus. |
Uma resposta 401 ou 400 do seu receptor não faz o Dracma desistir: toda resposta fora de 2xx provoca nova tentativa por até 72 h. Veja Retentativas, skipped e DLQ.
Veja também¶
- Envelope: campos do corpo.
- Receptor passo a passo: a ordem completa de verificação.
- Segredo e rotação: formato do
whsec_, rotação e vazamento. - Exemplos executáveis: verificação em Go, Node, PHP e shell.