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

# Signature Verification

> Verify webhook authenticity with HMAC-SHA256.

Every outbound webhook includes an `x-st-signature` header. Verify it before processing the event.

## Algorithm

```
x-st-signature = HMAC_SHA256(signingSecret, rawBody).hex()
```

| Item             | Value                                           |
| ---------------- | ----------------------------------------------- |
| Algorithm        | HMAC-SHA256                                     |
| Key              | Your webhook signing secret (e.g. `whsecd_...`) |
| Message          | Exact raw HTTP body bytes                       |
| Output           | Lowercase hex string (64 characters)            |
| Header           | `x-st-signature`                                |
| Algorithm header | `x-st-signature-alg: HMAC_SHA256_HEX`           |

<Warning>
  Sign the **exact raw body bytes** received over the wire. Do not re-format, pretty-print, or re-serialize parsed JSON — any change produces a different signature.
</Warning>

## Payload envelope

```json theme={null}
{
  "id": "<outboxId>",
  "type": "<event_type>",
  "data": { ... }
}
```

The body is compact JSON from `JSON.stringify()` with a fixed key order in `data` (see [Events](/webhooks/events)).

## Node.js example

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

function verifyWebhookSignature({ rawBody, signingSecret, signature }) {
  const expected = crypto
    .createHmac("sha256", signingSecret)
    .update(rawBody)
    .digest("hex");

  const received = Buffer.from(signature);
  const expectedBuffer = Buffer.from(expected);

  if (received.length !== expectedBuffer.length) {
    return false;
  }

  return crypto.timingSafeEqual(received, expectedBuffer);
}

// Express example — use raw body middleware
app.post("/webhooks/st", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-st-signature"];
  const isValid = verifyWebhookSignature({
    rawBody: req.body,
    signingSecret: process.env.WEBHOOK_SIGNING_SECRET,
    signature,
  });

  if (!isValid) {
    return res.status(401).send("Invalid signature");
  }

  const event = JSON.parse(req.body.toString());
  // Process event...
  res.sendStatus(200);
});
```

## Python example

```python theme={null}
import hmac
import hashlib

def verify_webhook_signature(raw_body: bytes, signing_secret: str, signature: str) -> bool:
    expected = hmac.new(
        signing_secret.encode(),
        raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature)
```

## Common pitfalls

| Pitfall                       | Result                                    |
| ----------------------------- | ----------------------------------------- |
| Pretty-printed JSON           | Signature mismatch                        |
| Re-stringifying parsed JSON   | Signature mismatch (key order may differ) |
| Trailing characters on secret | Signature mismatch                        |

## Endpoint verification

When you create or verify a webhook in the dashboard, a `webhook.endpoint_verification` event may be sent to confirm your endpoint accepts POST requests and returns HTTP 2xx.

## Response requirements

Your endpoint must:

* Accept `POST` with `Content-Type: application/json`
* Return HTTP **2xx** within 5 seconds
* Verify the signature before processing

Non-2xx responses and timeouts count as delivery failures and trigger retries with incremental backoff (60s first retry, then +40s each time).
