> ## Documentation Index
> Fetch the complete documentation index at: https://docs.yo-lead.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Verifying signatures

> Verify webhook payloads delivered by YoLead.

YoLead webhook requests include:

```http theme={null}
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:

```txt theme={null}
<timestamp>.<raw_body>
```

Use the webhook secret configured for your company. Reject requests with invalid signatures or stale timestamps.

<Warning>
  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.
</Warning>

Webhook timestamps must be within a 5-minute window.

<CodeGroup>
  ```ts Node.js theme={null}
  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 PHP theme={null}
  <?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);
  ```
</CodeGroup>
