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

# Verifying webhooks

> Check that a delivery came from LeadVector, unchanged, before you act on it.

LeadVector signs each delivery the way [Standard Webhooks](https://www.standardwebhooks.com) specifies, so its libraries verify it in one call. Each library checks the signature with your endpoint's secret and refuses a delivery whose `webhook-timestamp` is more than five minutes off your clock, which stops a captured request from being sent again later.

Verify the body exactly as it arrived: parse it only after the check. Re-serialized JSON can differ by a space and fail.

Then store the event somewhere durable (a queue, a database table) before you answer `2xx`, and answer an error if you can't: a `2xx` marks the delivery as succeeded, and LeadVector won't send it again. The examples below do the work after answering, from what they stored.

## With a library

```javascript Node.js theme={null}
// npm install standardwebhooks
import { Webhook } from 'standardwebhooks';

const webhook = new Webhook(process.env.LEADVECTOR_WEBHOOK_SECRET); // whsec_…

// Express: express.raw({ type: 'application/json' }) keeps the body as sent.
app.post('/leadvector', express.raw({ type: 'application/json' }), async (req, res) => {
  let event;
  try {
    event = webhook.verify(req.body.toString('utf8'), req.headers);
  } catch {
    return res.sendStatus(400);
  }
  try {
    await queue.add(event.id, event); // durable: survives a restart
  } catch {
    return res.sendStatus(503); // not stored: LeadVector sends it again
  }
  res.sendStatus(200); // a worker handles the queue
});
```

```python Python theme={null}
# pip install standardwebhooks
import os
from standardwebhooks.webhooks import Webhook, WebhookVerificationError

webhook = Webhook(os.environ["LEADVECTOR_WEBHOOK_SECRET"])  # whsec_…

# Flask
@app.post("/leadvector")
def leadvector():
    try:
        event = webhook.verify(request.get_data(as_text=True), dict(request.headers))
    except WebhookVerificationError:
        return "", 400
    try:
        queue.enqueue(event)  # durable: survives a restart
    except Exception:
        return "", 503  # not stored: LeadVector sends it again
    return "", 200  # a worker handles the queue
```

```php PHP theme={null}
<?php
// composer require standard-webhooks/standard-webhooks
use StandardWebhooks\Webhook;

$webhook = new Webhook(getenv('LEADVECTOR_WEBHOOK_SECRET')); // whsec_…

$headers = [
    'webhook-id' => $_SERVER['HTTP_WEBHOOK_ID'] ?? '',
    'webhook-timestamp' => $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '',
    'webhook-signature' => $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '',
];

try {
    $event = $webhook->verify(file_get_contents('php://input'), $headers);
} catch (Exception $e) {
    http_response_code(400);
    exit;
}

try {
    $queue->push($event); // durable: survives a restart
} catch (Exception $e) {
    http_response_code(503); // not stored: LeadVector sends it again
    exit;
}

http_response_code(200); // a worker handles the queue
```

## By hand

Without a library, the check is:

1. Take the secret after `whsec_` and decode it from base64: that is the key.
2. Build the signed content: `{webhook-id}.{webhook-timestamp}.{body}`, with the raw body.
3. Compute HMAC-SHA256 of it with the key, and encode the result in base64.
4. `webhook-signature` holds one or more signatures, space-separated, each `v1,` and a base64 value. The delivery is genuine if any of them equals yours. Compare in constant time.
5. Refuse it if `webhook-timestamp` (Unix seconds) is more than five minutes from now.

```javascript theme={null}
import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(secret, headers, body) {
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const signed = `${headers['webhook-id']}.${headers['webhook-timestamp']}.${body}`;
  const expected = createHmac('sha256', key).update(signed).digest();
  const age = Math.abs(Date.now() / 1000 - Number(headers['webhook-timestamp']));
  if (!(age <= 300)) return false;
  return headers['webhook-signature'].split(' ').some((entry) => {
    const [version, value] = entry.split(',');
    const given = Buffer.from(value ?? '', 'base64');
    return version === 'v1' && given.length === expected.length && timingSafeEqual(given, expected);
  });
}
```

## When a rotation overlaps

While a rotated secret's old one still signs (`overlapHours`), each delivery carries a signature from each, and one that either secret verifies is genuine. Switch your receiver to the new secret any time in that window; the libraries above accept a delivery if any of its signatures matches the secret you gave them.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.