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

# Signature Verification

> Verify that a webhook delivery genuinely came from Struct using its HMAC signature.

When you set a `secret` on a webhook subscription, Struct signs every delivery with an HMAC so you can confirm the request is genuine and the body was not tampered with in transit.

<Warning>Always verify signatures in production. Your endpoint is a public URL — without verification, anyone who discovers it can POST forged events to it.</Warning>

## The signature header

Each delivery includes:

```
X-Webhook-Signature: sha256=<hex>
```

* **Algorithm:** HMAC-SHA256.
* **Key:** the `secret` you set when creating (or rotating) the webhook.
* **Message:** the **raw request body bytes**, exactly as received — do not re-serialize the JSON before computing the HMAC.
* **Encoding:** lowercase hex, prefixed with `sha256=`.

There is no timestamp component, so you verify the body alone.

## How to verify

Compute the HMAC-SHA256 of the raw body with your secret, prefix it with `sha256=`, and compare it to the header using a **constant-time** comparison.

<CodeGroup>
  ```javascript Node.js (Express) theme={null}
  import crypto from "node:crypto";
  import express from "express";

  const app = express();
  const SECRET = process.env.WEBHOOK_SECRET;

  app.post(
    "/webhooks",
    express.raw({ type: "application/json" }), // keep the raw body
    (req, res) => {
      const received = req.get("X-Webhook-Signature") || "";
      const expected =
        "sha256=" +
        crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");

      const a = Buffer.from(received);
      const b = Buffer.from(expected);
      if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
        return res.status(401).send("invalid signature");
      }

      const event = JSON.parse(req.body.toString("utf8"));
      res.sendStatus(200); // ack fast, then process
      handleEvent(event);
    },
  );
  ```

  ```python Python (FastAPI) theme={null}
  import hmac
  import hashlib
  import os
  from fastapi import FastAPI, Request, Response

  app = FastAPI()
  SECRET = os.environ["WEBHOOK_SECRET"].encode()

  @app.post("/webhooks")
  async def webhooks(request: Request):
      body = await request.body()  # raw bytes
      received = request.headers.get("X-Webhook-Signature", "")
      expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()

      if not hmac.compare_digest(received, expected):
          return Response(status_code=401)

      event = await request.json()
      # ack fast, then process out of band
      return Response(status_code=200)
  ```
</CodeGroup>

<Note>Frameworks that auto-parse JSON often discard the raw body. Make sure you hash the **bytes as received** — re-serializing the parsed object can reorder keys or change whitespace and break the signature.</Note>

## Other delivery headers

Every delivery also carries these headers (handy for logging and routing):

| Header                | Description                                                                                                                                |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `X-Webhook-ID`        | UUID of the webhook subscription that produced the delivery.                                                                               |
| `X-Delivery-ID`       | UUID of this specific delivery. Matches the `id` field in the body — use it to deduplicate.                                                |
| `X-Event-Type`        | The event name, e.g. `trader_whale_trade`.                                                                                                 |
| `X-Attempt`           | Delivery attempt number, starting at `1`. Struct retries failed deliveries — see the [`attempt` field](/webhooks/response-format#attempt). |
| `X-Webhook-Signature` | The HMAC signature described above (only present when a `secret` is set).                                                                  |

## Rotating your secret

If a secret may have leaked, rotate it. New deliveries are signed with the new secret immediately:

```bash theme={null}
curl -X POST https://api.struct.to/v1/webhooks/{webhook_id}/rotate-secret \
  -H "X-API-Key: YOUR_API_KEY"
```
