Verifique a assinatura dos webhooks
X-Domu-Signature em Node, Python, PHP e Go, com um vetor de teste para conferir seu código.
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.
X-Domu-SignatureX-Webhook-SignatureX-Domu-EventX-Webhook-AttemptX-Webhook-IdX-Webhook-Test: trueUser-Agent: Domu-Webhooks/2Verifique 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.
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: 1028740337f17ec37cd46ba461bec24105784b249b9280f87f56436376a869c9Se 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.