Skip to main content
YoLead webhook requests include:
X-YoLead-Webhook-Id: <event_id>
X-YoLead-Timestamp: <unix_ms_timestamp>
X-YoLead-Signature: <hex_hmac_sha256>
Verify the signature with HMAC-SHA256 over:
<timestamp>.<raw_body>
Use the webhook secret configured for your company. Reject requests with invalid signatures or stale timestamps.
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.
Webhook timestamps must be within a 5-minute window.
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);
  }
);