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

# Verify webhook signatures

> Check that a webhook came from Darwin: Standard Webhooks HMAC-SHA256 signatures, the five-minute timestamp window, code for Node and Python, Svix and the TypeScript SDK, and rotating the signing secret.

Darwin signs every request the [Standard Webhooks](https://www.standardwebhooks.com/) way, so you can use an existing library or a few lines of your own. Always verify before you trust a payload: anyone can send a `POST` to your URL.

1. Take the **raw request body** exactly as received. Parsing and re-serialising the JSON changes the bytes and breaks the signature.
2. Reject the request if `webhook-timestamp` is more than five minutes from your clock.
3. Build the signed content: `` `${webhook-id}.${webhook-timestamp}.${body}` ``.
4. Compute HMAC-SHA256 over it. The key is your signing secret **without** the `whsec_` prefix, base64-decoded.
5. Base64-encode the result and compare it, in constant time, with each `v1,` entry in `webhook-signature`. The header can hold several entries separated by spaces (during a [secret rotation](#rotate-the-signing-secret)); accept if any matches.

```javascript Node (Express) theme={null}
import crypto from "node:crypto";
import express from "express";

const secret = process.env.DARWIN_WEBHOOK_SECRET; // whsec_...

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

const app = express();
app.post("/webhooks/darwin", express.raw({ type: "application/json" }), (req, res) => {
  const body = req.body.toString("utf8");
  if (!verify(req.headers, body)) return res.status(400).send("bad signature");
  const event = JSON.parse(body);
  res.sendStatus(200); // answer first, work after
  queueForProcessing(event); // your own job queue
});
app.listen(3000);
```

```python Python (Flask) theme={null}
import base64, hashlib, hmac, json, os, time
from flask import Flask, request, abort

secret = os.environ["DARWIN_WEBHOOK_SECRET"]  # whsec_...
key = base64.b64decode(secret.removeprefix("whsec_"))
app = Flask(__name__)

def verify(headers, body: bytes) -> bool:
    msg_id = headers.get("webhook-id")
    timestamp = headers.get("webhook-timestamp")
    signatures = headers.get("webhook-signature", "")
    if not msg_id or not timestamp or abs(time.time() - int(timestamp)) > 300:
        return False
    signed = f"{msg_id}.{timestamp}.".encode() + body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
    return any(
        entry.startswith("v1,") and hmac.compare_digest(entry[3:], expected)
        for entry in signatures.split(" ")
    )

@app.post("/webhooks/darwin")
def darwin_webhook():
    body = request.get_data()  # raw bytes
    if not verify(request.headers, body):
        abort(400)
    event = json.loads(body)
    queue_for_processing(event)  # your own job queue
    return "", 200
```

### Use a Standard Webhooks library

Any Standard Webhooks library verifies Darwin's signatures, including Svix's. Pass the raw body, the headers and the secret as Darwin shows it:

```javascript Node (svix) theme={null}
import { Webhook } from "svix";

const wh = new Webhook(process.env.DARWIN_WEBHOOK_SECRET); // whsec_...
// Throws if the signature or timestamp is wrong.
const event = wh.verify(rawBody, {
  "webhook-id": req.headers["webhook-id"],
  "webhook-timestamp": req.headers["webhook-timestamp"],
  "webhook-signature": req.headers["webhook-signature"],
});
```

```python Python (svix) theme={null}
from svix.webhooks import Webhook

wh = Webhook(os.environ["DARWIN_WEBHOOK_SECRET"])  # whsec_...
event = wh.verify(raw_body, request.headers)  # raises if invalid
```

With the [TypeScript SDK](https://docs.meetdarwin.ai/sdks/typescript/webhooks.md), `await Darwin.webhooks.verify(body, headers, secret)` does all of this.

## Rotate the signing secret

Open the endpoint's **⋯** menu, select **Rotate signing secret** and confirm. Darwin shows the new secret once. For the next **24 hours** every delivery carries two signatures, one per secret, so your receiver keeps working while you switch it to the new secret. After 24 hours only the new secret signs.

With the API: `POST /v1/webhook_endpoints/{id}/rotate_secret` returns the endpoint with its new `secret`.


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