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

# Verify Webhook Signatures

> Verify Brale webhook deliveries by comparing the x-request-signature-sha-256 header against an HMAC-SHA256 of the raw request body using your shared secret.

## Signature header

Brale signs webhook deliveries with HMAC-SHA256.

Each request includes:

```
x-request-signature-sha-256: <lowercase hex HMAC-SHA256>
```

The signature is computed over the exact raw request body bytes.

Signature verification details:

* The header value is the lowercase hex-encoded HMAC-SHA256 digest (no prefix like `v1=`, no version, no timestamp).
* The HMAC is computed over the exact raw request body bytes (no canonicalization).
* The HMAC key is the Base64URL-decoded `sharedSecret` value returned when creating the subscription.

## Shared secret

When you create a webhook subscription, Brale returns a `sharedSecret`.

```json theme={null}
{
  "sharedSecret": "base64url-encoded-secret"
}
```

This value is Base64URL encoded. Decode it before using it as the HMAC key.

Do not use the encoded string directly as the HMAC key.

## Which secret should I use?

Each subscription has its own `sharedSecret`. Verify each delivery with the secret of the subscription that delivered it. Don't look up a secret based on `data.account_id`.

* With one managing-account subscription, use that subscription's secret for every event it delivers. This includes events concerning the managing account and events concerning its managed accounts.
* With multiple subscriptions, verify each delivery with the secret of the corresponding subscription.

Brale returns the `sharedSecret` only once, when you create the subscription. Store it immediately.

## Node.js / TypeScript example

```tsx theme={null}
import crypto from "node:crypto";

export function verifyBraleWebhookSignature({
  rawBody,
  sharedSecret,
  signatureHeader,
}: {
  rawBody: Buffer;
  sharedSecret: string;
  signatureHeader: string | undefined;
}): boolean {
  if (!signatureHeader) return false;

  const key = Buffer.from(sharedSecret, "base64url");

  const expectedHex = crypto
    .createHmac("sha256", key)
    .update(rawBody)
    .digest("hex");

  const expected = Buffer.from(expectedHex, "hex");
  const received = Buffer.from(signatureHeader, "hex");

  if (expected.length !== received.length) return false;

  return crypto.timingSafeEqual(expected, received);
}
```

### Express example

Mount the webhook route with `express.raw()` before `express.json()`.

```tsx theme={null}
import express from "express";
import { verifyBraleWebhookSignature } from "./verify";

const app = express();

app.post(
  "/webhooks/brale",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const rawBody = req.body as Buffer;
    const signature = req.header("x-request-signature-sha-256");

    const verified = verifyBraleWebhookSignature({
      rawBody,
      sharedSecret: process.env.BRALE_WEBHOOK_SHARED_SECRET!,
      signatureHeader: signature,
    });

    if (!verified) {
      return res.status(401).json({ error: "invalid_signature" });
    }

    const event = JSON.parse(rawBody.toString("utf8"));

    // Deduplicate before applying side effects.
    // await ensureNotProcessed(event.id);

    // Process event...
    return res.status(200).json({ ok: true });
  }
);

// Mount JSON middleware after the webhook route.
app.use(express.json());
```

## Python example

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

def base64url_decode(value: str) -> bytes:
    padding = "=" * (-len(value) % 4)
    return base64.urlsafe_b64decode(value + padding)

def verify_brale_signature(raw_body: bytes, shared_secret: str, signature_header: str) -> bool:
    key = base64url_decode(shared_secret)
    expected = hmac.new(key, raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature_header)
```

## Common signature verification failures

| Symptom | Likely cause | Fix |
| - | - | - |
| `401 invalid_signature` | Using the wrong secret | Confirm you copied the `sharedSecret` from the same subscription receiving the event |
| `401 invalid_signature` after updating `.env` | Server still running with old env | Restart your server |
| `401 invalid_signature` in Express | You verified after JSON parsing | Capture raw body with `express.raw()` before `express.json()` |
| `401 invalid_signature` in tests | Signed one serialized body but sent another | Sign the exact bytes you send |
| Signature length mismatch | Header is missing or malformed | Validate header exists before comparing |
| Works locally but not in production | Different env var or subscription secret | Confirm production env uses the correct `sharedSecret` |
