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

# Verify Signatures

> Authenticate webhook deliveries with the X-Way-Signature header

When your webhook has a secret configured, every delivery includes an `X-Way-Signature` header:

```
X-Way-Signature: qL2vXt0k9pZ8w1Jm3RfB6yUcNdEaGhSiOo4T7rVKxAY=
```

It is the **HMAC-SHA256** of the request body, keyed with your webhook secret, **base64-encoded**. Recompute it and compare to prove the request came from Way and wasn't tampered with.

<Warning>
  **Compute the HMAC over the raw request body bytes - not a re-serialized copy.** Parsing the JSON and re-stringifying it (`JSON.stringify(req.body)`) can produce different bytes than what Way signed (number formatting, key handling, and unicode escaping vary by language and parser), which makes verification fail intermittently or - worse - pass only by coincidence. Capture the body before your framework parses it.
</Warning>

## Node.js (Express)

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

const app = express();

app.post(
  "/webhooks/way",
  // keep the raw bytes for signature verification
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.get("X-Way-Signature") ?? "";
    const expected = crypto
      .createHmac("sha256", process.env.WAY_WEBHOOK_SECRET)
      .update(req.body) // req.body is a Buffer of the raw bytes
      .digest("base64");

    const valid =
      signature.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));

    if (!valid) return res.status(401).end();

    res.status(200).end(); // acknowledge fast...
    const { event, payload } = JSON.parse(req.body); // ...then process
    handleEvent(event, payload);
  }
);
```

## Python (Flask)

```python theme={null}
import base64, hashlib, hmac, os
from flask import Flask, request

app = Flask(__name__)

@app.post("/webhooks/way")
def way_webhook():
    raw = request.get_data()  # raw bytes, before JSON parsing
    expected = base64.b64encode(
        hmac.new(os.environ["WAY_WEBHOOK_SECRET"].encode(), raw, hashlib.sha256).digest()
    ).decode()

    if not hmac.compare_digest(request.headers.get("X-Way-Signature", ""), expected):
        return "", 401

    body = request.get_json()
    handle_event(body["event"], body["payload"])
    return "", 200
```

## Pitfalls

* **Body re-serialization** - the most common failure. Frameworks that eagerly parse JSON (Express's `express.json()`, some serverless runtimes) must be configured to expose the raw body for the webhook route.
* **Proxies that mutate the body** - anything that re-encodes or pretty-prints the JSON between Way and your handler breaks the signature. Verify at the first hop that sees the raw bytes.
* **Missing header** - deliveries carry `X-Way-Signature` only when the webhook has a secret configured. If you expect it and it's absent, treat the request as unauthenticated.
* **String comparison** - use a timing-safe comparison (`crypto.timingSafeEqual`, `hmac.compare_digest`) rather than `===`.
