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).
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 });
});