ByDomuDevelopers

Verifique a assinatura dos webhooks

X-Domu-Signature em Node, Python, PHP e Go, com um vetor de teste para conferir seu código.

GuiaNode · Python · PHP · GoHMAC-SHA256

Como cada entrega é assinada

X-Domu-Signature: t=<unix>,v1=<hex>. v1 é o HMAC-SHA256, em hexadecimal, de «t» + «.» + o corpo cru, com o secret da assinatura (o que POST /webhooks devolveu uma única vez, ou o de Tickets na sua tela). Rejeite carimbos com mais de 5 minutos para que ninguém repita uma entrega antiga.

CabeçalhoValor
Assinatura com carimbo de tempo (use esta)X-Domu-Signature
Assinatura legada: HMAC só do corpoX-Webhook-Signature
Tipo do evento (ou nome legado)X-Domu-Event
Número da tentativa (1 a 5)X-Webhook-Attempt
Id da assinaturaX-Webhook-Id
Só nas entregas de testeX-Webhook-Test: true
Quem enviaUser-Agent: Domu-Webhooks/2

Verifique no seu servidor

Os quatro fazem o mesmo: leem o corpo cru, separam t e v1, conferem a idade, calculam o HMAC e comparam em tempo constante.

import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

const app = express();
const SECRET = process.env.BYDOMU_WEBHOOK_SECRET;

// El cuerpo CRUDO: firmar el JSON ya parseado y vuelto a serializar no coincide.
app.post("/webhooks/bydomu", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req.body.toString("utf8"), req.get("X-Domu-Signature") ?? "")) {
    return res.status(401).end();
  }
  res.status(200).end();                   // responde antes de 10 s…
  handle(JSON.parse(req.body.toString())); // …y procesa aparte
});

function verify(rawBody, header, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=")));
  const t = Number(parts.t);
  if (!t || !parts.v1 || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
  const expected = createHmac("sha256", SECRET).update(`${t}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1);
  return a.length === b.length && timingSafeEqual(a, b);
}

Teste seu código com este vetor

Com este secret, este carimbo e este corpo, sua função deve calcular exatamente estas assinaturas (sem a checagem de idade, porque o carimbo é antigo). Depois use «Enviar um teste» do webhook: essa entrega traz X-Webhook-Test: true e data com uma mensagem.

Vetor de teste
secret    whsec_prueba_0000000000000000
timestamp 1790000000
body      {"event":"deal.won","id":"5f3a0c1e-7d2b-4f7e-9a51-0b6c1d2e3f40","type":"deal.won"}

X-Domu-Signature:    t=1790000000,v1=d855533ad9162950679a857a7fa61761e42dd7ac6438bc64b4640097d51540b3
X-Webhook-Signature: 1028740337f17ec37cd46ba461bec24105784b249b9280f87f56436376a869c9

Se a assinatura não bater

  • bodyVocê está assinando o JSON parseado. Use os bytes como chegaram (express.raw, request.get_data(), php://input, io.ReadAll).
  • keyO secret não é o dessa assinatura, ou você o rotacionou e o servidor tem o anterior.
  • timeO relógio do seu servidor está mais de 5 minutos fora: sincronize (NTP).
  • 2xxSe você responder devagar ou com erro, há nova tentativa: Até 5 tentativas (0 s, 5 s, 30 s, 2 min e 10 min) em erro de rede, 429 ou 5xx. Responda 2xx em menos de 10 s.