Skip to main content

Delivery and headers

Braid sends POST requests to your registered url with:
  • Content-Type: application/json
  • Braid-Event-Id
  • Braid-Event-Type
  • Braid-Signature
Body is the JSON event payload (for example, a deposit object or withdrawal object).

Signature format

The webhook signing key (shared secret) is returned byPOST /webhooks/registrations as the secret field. Use this secret to verify webhook payloads are from Braid servers.
  • Header: Braid-Signature: t=<unix_timestamp>,v1=<hex_hmac>
  • HMAC: v1 = HMAC_SHA256(key = secret, message = t + "." + rawBody)
  • rawBody is the exact request body string (verify before JSON.parse)

Node / Express verification example

// Express example
const crypto = require("crypto");
const express = require("express");

function verifyBraidSignature(rawBody, signatureHeader, signingSecret, toleranceSeconds = 300) {
  if (!signatureHeader) throw new Error("Missing Braid-Signature header");

  const parts = signatureHeader.split(",");
  const tPart = parts.find((p) => p.startsWith("t="));
  const v1Part = parts.find((p) => p.startsWith("v1="));
  if (!tPart || !v1Part) throw new Error("Invalid Braid-Signature header format");

  const timestamp = Number(tPart.split("=")[1]);
  const signature = v1Part.split("=")[1];

  // Optional replay protection
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - timestamp) > toleranceSeconds) {
    throw new Error("Braid-Signature timestamp outside allowed window");
  }

  const payloadToSign = `${timestamp}.${rawBody}`;
  const computed = crypto.createHmac("sha256", signingSecret).update(payloadToSign).digest("hex");

  const expected = Buffer.from(computed, "utf8");
  const actual = Buffer.from(signature, "utf8");
  if (expected.length !== actual.length || !crypto.timingSafeEqual(expected, actual)) {
    throw new Error("Invalid Braid-Signature");
  }
}

const app = express();

app.post("/braid/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const rawBody = req.body.toString("utf8");
  const signatureHeader = req.get("Braid-Signature");
  const signingSecret = process.env.BRAID_WEBHOOK_SECRET;

  try {
    verifyBraidSignature(rawBody, signatureHeader, signingSecret);
  } catch (err) {
    return res.status(400).json({ error: "Invalid signature" });
  }

  const event = JSON.parse(rawBody);
  // Use event.event to route handling:
  // - "portfolio_wallet.withdrawal.status_changed"
  // - "portfolio_wallet.deposit.status_changed"

  return res.status(200).json({ received: true });
});