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 prefixo whsec_, 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 do t=, sem sinal e sem zeros à esquerda, seguido de um ponto (.).
  • corpo_cru sã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 v1 vem 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óprio 0 é 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 mesmo id e 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 v1 usa 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º v1 bate.
  • Um receptor que ainda tem só o segredo anterior também aceita, porque o 2º v1 bate. É isso que permite rotacionar sem perder nenhuma entrega.

Vetor 3: negativo (DEVE recusar)

  • O corpo tem um byte alterado ("object_version":4 no lugar de 3), mas o header é a assinatura do vetor 1.
  • O HMAC correto desse corpo seria 47ab23d3…c77a6995, e ele não bate com 4f82d5da…. 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