Verifica la firma de los webhooks
X-Domu-Signature en Node, Python, PHP y Go, con un vector de prueba para comprobar tu código.
Cómo se firma cada entrega
X-Domu-Signature: t=<unix>,v1=<hex>. v1 es el HMAC-SHA256, en hexadecimal, de «t» + «.» + el cuerpo crudo, con el secret de la suscripción (el que devolvió POST /webhooks una sola vez, o el de Tickets en su pantalla). Rechaza marcas de más de 5 minutos para que nadie repita una entrega vieja.
X-Domu-SignatureX-Webhook-SignatureX-Domu-EventX-Webhook-AttemptX-Webhook-IdX-Webhook-Test: trueUser-Agent: Domu-Webhooks/2Verifícala en tu servidor
Los cuatro hacen lo mismo: leen el cuerpo crudo, separan t y v1, revisan la edad, calculan el HMAC y comparan en tiempo 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);
}Prueba tu código con este vector
Con este secret, esta marca y este cuerpo, tu función debe calcular exactamente estas firmas (sin la revisión de edad, porque la marca es vieja). Luego usa «Enviar una prueba» del webhook: esa entrega trae X-Webhook-Test: true y data con un mensaje.
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: 1028740337f17ec37cd46ba461bec24105784b249b9280f87f56436376a869c9Si la firma no coincide
- bodyEstás firmando el JSON parseado. Usa los bytes tal como llegaron (express.raw, request.get_data(), php://input, io.ReadAll).
- keyEl secret no es el de esa suscripción, o lo rotaste y tu servidor tiene el anterior.
- timeEl reloj de tu servidor está corrido más de 5 minutos: sincronízalo (NTP).
- 2xxSi respondes lento o con error, se reintenta: Hasta 5 intentos (0 s, 5 s, 30 s, 2 min y 10 min) ante error de red, 429 o 5xx. Responde 2xx en menos de 10 s.