X-YoLead-Webhook-Id: <event_id>
X-YoLead-Timestamp: <unix_ms_timestamp>
X-YoLead-Signature: <hex_hmac_sha256>
<timestamp>.<raw_body>
Always verify the signature against the raw request body before parsing JSON. Re-serializing parsed JSON can change whitespace or key order and produce a different signature.
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
const app = express();
const webhookSecret = process.env.YOLEAD_WEBHOOK_SECRET!;
const timestampWindowMs = 5 * 60 * 1000;
app.post(
"/yolead/webhook",
express.raw({ type: "application/json" }),
(req, res) => {
const timestamp = req.header("X-YoLead-Timestamp");
const signature = req.header("X-YoLead-Signature");
const webhookId = req.header("X-YoLead-Webhook-Id");
if (!timestamp || !signature || !webhookId) {
return res.sendStatus(401);
}
const parsedTimestamp = Number(timestamp);
if (
!Number.isSafeInteger(parsedTimestamp) ||
Math.abs(Date.now() - parsedTimestamp) > timestampWindowMs
) {
return res.sendStatus(401);
}
const rawBody = req.body as Buffer;
const expected = createHmac("sha256", webhookSecret)
.update(`${timestamp}.`)
.update(rawBody)
.digest("hex");
const expectedBuffer = Buffer.from(expected, "hex");
const receivedBuffer = Buffer.from(signature, "hex");
if (
expectedBuffer.length !== receivedBuffer.length ||
!timingSafeEqual(expectedBuffer, receivedBuffer)
) {
return res.sendStatus(401);
}
const event = JSON.parse(rawBody.toString("utf8"));
// Store event.id or X-YoLead-Webhook-Id for idempotency.
return res.sendStatus(200);
}
);