Skip to main content

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

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.
{
  "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.

Node.js / TypeScript example

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().
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

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

SymptomLikely causeFix
401 invalid_signatureUsing the wrong secretConfirm you copied the sharedSecret from the same subscription receiving the event
401 invalid_signature after updating .envServer still running with old envRestart your server
401 invalid_signature in ExpressYou verified after JSON parsingCapture raw body with express.raw() before express.json()
401 invalid_signature in testsSigned one serialized body but sent anotherSign the exact bytes you send
Signature length mismatchHeader is missing or malformedValidate header exists before comparing
Works locally but not in productionDifferent env var or subscription secretConfirm production env uses the correct sharedSecret