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

# Verifying Webhooks

> How to verify webhook signatures to ensure payloads are authentic

Every webhook delivery includes an `X-Signature` header so you can confirm it came from Etherfuse and wasn't tampered with.

## How It Works

When you [create a webhook](/api-reference/webhooks/create-webhook), the response includes a `secret` — a base64-encoded HMAC key. **Store it securely; it is only returned once.**

Each delivery signs the payload with that secret:

1. The JSON body is **canonicalized** ([RFC 8785 JCS](https://datatracker.ietf.org/doc/html/rfc8785) — deterministic key ordering, no extra whitespace)
2. HMAC-SHA256 is computed over the canonicalized string using your secret (decoded from base64)
3. The result is sent as `X-Signature: sha256={hex}`

## Verifying the Signature

1. **Canonicalize** the received JSON body
2. **Decode** your webhook secret from base64
3. **Compute** HMAC-SHA256 over the canonicalized string
4. **Compare** `sha256={hex_result}` to the `X-Signature` header using a constant-time comparison

<Warning>
  The signature is computed over the **canonicalized** JSON, not the raw request body. You must canonicalize before comparing or the signature will not match.
</Warning>

### Node.js

```javascript theme={null}
import { createHmac, timingSafeEqual } from "crypto";
import canonicalize from "canonicalize"; // npm install canonicalize

function verifyWebhook(body, secret, signatureHeader) {
  const canonicalized = canonicalize(body);
  const key = Buffer.from(secret, "base64");
  const hmac = createHmac("sha256", key).update(canonicalized).digest("hex");
  const expected = `sha256=${hmac}`;

  if (expected.length !== signatureHeader.length) return false;
  return timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}

// Usage in an Express handler:
app.post("/webhook", (req, res) => {
  const signature = req.headers["x-signature"];
  if (!verifyWebhook(req.body, process.env.WEBHOOK_SECRET, signature)) {
    return res.status(401).send("Invalid signature");
  }
  // Process the event...
  res.sendStatus(200);
});
```

### Python

```python theme={null}
import hmac, hashlib, base64
from canonicaljson import encode_canonical_json  # pip install canonicaljson

def verify_webhook(body: dict, secret: str, signature_header: str) -> bool:
    canonicalized = encode_canonical_json(body)
    key = base64.b64decode(secret)
    digest = hmac.new(key, canonicalized, hashlib.sha256).hexdigest()
    expected = f"sha256={digest}"
    return hmac.compare_digest(expected, signature_header)
```

## Delivery & Retries

Failed deliveries (non-2xx responses or connection errors) are retried up to **3 times** with **5-second delays** between attempts. Return a 2xx promptly to avoid unnecessary retries.
