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);
}
);
<?php
$webhookSecret = getenv('YOLEAD_WEBHOOK_SECRET');
$timestampWindowMs = 5 * 60 * 1000;
$timestamp = $_SERVER['HTTP_X_YOLEAD_TIMESTAMP'] ?? null;
$signature = $_SERVER['HTTP_X_YOLEAD_SIGNATURE'] ?? null;
$webhookId = $_SERVER['HTTP_X_YOLEAD_WEBHOOK_ID'] ?? null;
$rawBody = file_get_contents('php://input');
if (!$timestamp || !$signature || !$webhookId || $rawBody === false) {
http_response_code(401);
exit;
}
$parsedTimestamp = filter_var($timestamp, FILTER_VALIDATE_INT);
$currentTimestamp = (int) round(microtime(true) * 1000);
if ($parsedTimestamp === false || abs($currentTimestamp - $parsedTimestamp) > $timestampWindowMs) {
http_response_code(401);
exit;
}
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $webhookSecret);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit;
}
$event = json_decode($rawBody, true);
if (!is_array($event)) {
http_response_code(400);
exit;
}
// Store $event['id'] or $webhookId for idempotency.
http_response_code(200);