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

# Verify signatures

> Confirm that incoming webhook requests genuinely originated from Openlayer.

Because your webhook endpoint is a public URL, anyone could send requests to it.
To confirm that a request genuinely came from Openlayer and was not tampered
with, every delivery is signed. Your endpoint should **verify the signature
before processing the payload**.

Openlayer follows the [Standard Webhooks](https://www.standardwebhooks.com/)
specification, so you can verify signatures with any compatible library.

## Signature headers

Every webhook request includes the following headers:

| Header              | Description                                                                                 |
| ------------------- | ------------------------------------------------------------------------------------------- |
| `webhook-id`        | A unique identifier for the message. It stays constant across retries of the same event.    |
| `webhook-timestamp` | The Unix timestamp (in seconds) of the delivery attempt.                                    |
| `webhook-signature` | A space-delimited list of signatures, each prefixed with its version (for example, `v1,…`). |

Requests are also sent with `Content-Type: application/json` and a
`User-Agent` of `Openlayer-Webhooks/1.0`.

## How the signature is computed

The signature is an HMAC-SHA256 over the webhook id, timestamp, and the raw
request body, joined with periods:

```text theme={null}
signed_content = "{webhook-id}.{webhook-timestamp}.{raw_body}"
```

The key is your subscription's signing secret with the `whsec_` prefix removed
and the remainder Base64-decoded. The result is Base64-encoded and prefixed with
`v1,` to form the value sent in the `webhook-signature` header:

```text theme={null}
signature = "v1," + base64(HMAC_SHA256(base64decode(secret_without_prefix), signed_content))
```

<Warning>
  Verify against the **raw request body** exactly as received. Parsing the JSON
  and re-serializing it can change the bytes (key order, whitespace) and cause
  verification to fail.
</Warning>

## Verify with a library (recommended)

The [Standard Webhooks](https://www.standardwebhooks.com/) libraries handle
signature construction, Base64 decoding, constant-time comparison, and timestamp
checks for you. Pass the signing secret returned when you created the
subscription.

<CodeGroup>
  ```python Python theme={null}
  # pip install standardwebhooks
  from standardwebhooks import Webhook

  # The secret returned when the subscription was created, e.g. "whsec_..."
  secret = "YOUR_WEBHOOK_SIGNING_SECRET"

  def handle_request(raw_body: bytes, headers: dict):
      wh = Webhook(secret)
      # Raises an error if the signature or timestamp is invalid.
      payload = wh.verify(raw_body, {
          "webhook-id": headers["webhook-id"],
          "webhook-timestamp": headers["webhook-timestamp"],
          "webhook-signature": headers["webhook-signature"],
      })
      # payload is the verified, parsed event.
      return payload
  ```

  ```javascript JavaScript theme={null}
  // npm install standardwebhooks
  import { Webhook } from "standardwebhooks";

  // The secret returned when the subscription was created, e.g. "whsec_..."
  const secret = "YOUR_WEBHOOK_SIGNING_SECRET";

  function handleRequest(rawBody, headers) {
    const wh = new Webhook(secret);
    // Throws if the signature or timestamp is invalid.
    const payload = wh.verify(rawBody, {
      "webhook-id": headers["webhook-id"],
      "webhook-timestamp": headers["webhook-timestamp"],
      "webhook-signature": headers["webhook-signature"],
    });
    // payload is the verified, parsed event.
    return payload;
  }
  ```
</CodeGroup>

## Verify manually

If you prefer not to add a dependency, you can reproduce the signature yourself
and compare it to the header using a constant-time comparison.

<CodeGroup>
  ```python Python theme={null}
  import base64
  import hashlib
  import hmac

  def verify(raw_body: bytes, headers: dict, secret: str) -> bool:
      webhook_id = headers["webhook-id"]
      timestamp = headers["webhook-timestamp"]

      signed_content = f"{webhook_id}.{timestamp}.{raw_body.decode('utf-8')}"
      key = base64.b64decode(secret.removeprefix("whsec_"))
      digest = hmac.new(key, signed_content.encode("utf-8"), hashlib.sha256).digest()
      expected = base64.b64encode(digest).decode("utf-8")

      # The header may contain multiple space-delimited signatures.
      for part in headers["webhook-signature"].split(" "):
          version, _, value = part.partition(",")
          if version == "v1" and hmac.compare_digest(value, expected):
              return True
      return False
  ```

  ```javascript JavaScript theme={null}
  import crypto from "crypto";

  function verify(rawBody, headers, secret) {
    const webhookId = headers["webhook-id"];
    const timestamp = headers["webhook-timestamp"];

    const signedContent = `${webhookId}.${timestamp}.${rawBody}`;
    const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
    const expected = crypto
      .createHmac("sha256", key)
      .update(signedContent)
      .digest("base64");

    // The header may contain multiple space-delimited signatures.
    return headers["webhook-signature"].split(" ").some((part) => {
      const [version, value] = part.split(",");
      return (
        version === "v1" &&
        value.length === expected.length &&
        crypto.timingSafeEqual(Buffer.from(value), Buffer.from(expected))
      );
    });
  }
  ```
</CodeGroup>

## Replay protection

The `webhook-timestamp` header lets you reject stale requests. Compare it to the
current time and discard requests whose timestamp is outside a tolerance window
(for example, more than 5 minutes old). The Standard Webhooks libraries perform
this check for you.

## Idempotency

Because deliveries are [retried](/security/webhooks/overview#retries), your
endpoint may receive the same event more than once. The `webhook-id` header is
constant across retries of the same event, so you can use it as an idempotency
key — record the IDs you've already processed and skip duplicates.
