Agent skill
Braze Webhooks Skill
Receive and authenticate Braze webhooks. Use when setting up an endpoint for Braze webhook campaigns, Canvas webhook steps or webhook templates, verifying the Basic authentication credential Braze sends, debugging 401s, or handling the JSON or raw text request bodies your messages compose.
Install this skill
npx skills add hookdeck/webhook-skills --skill braze-webhooks
Braze sends an outbound HTTP request from a webhook campaign, a Canvas webhook step or a webhook template when a user reaches that message. These webhooks are not signed and have no event field: the marketer chooses the URL, the HTTP method and the request body. To authenticate the sender, attach a Basic authentication credential (Settings > Credential Management) to the webhook. Braze then sends Authorization: Basic base64(username:password), and your endpoint compares it with the username and password you stored.
This skill covers webhook campaigns, Canvas steps and templates, not Braze Currents. Token credentials and manually entered headers are not covered.
A static shared credential authenticates the sender, not the body, and gives no replay protection. There is no signature and no timestamp, so anyone who captures a request can resend it. Serve the endpoint over HTTPS only, and rotate the credential if it leaks.
When to Use This Skill
- How do I receive Braze webhooks?
- How do I authenticate a Braze webhook campaign or Canvas webhook step?
- Does Braze sign webhook requests? (No: use a Basic authentication credential)
- Why does my Braze webhook endpoint return 401?
- How do I handle a Braze webhook with a GET, PUT or DELETE method or an empty body?
Verification (core)
const crypto = require('crypto');
// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
const safeEqual = (a, b) => crypto.timingSafeEqual(
crypto.createHash('sha256').update(a).digest(),
crypto.createHash('sha256').update(b).digest());
function verifyBrazeRequest(headers, user = process.env.BRAZE_WEBHOOK_USERNAME,
pass = process.env.BRAZE_WEBHOOK_PASSWORD) {
if (!user || !pass) return false; // never accept when unconfigured
const m = /^basic\s+([A-Za-z0-9+/]+={0,2})$/i.exec((headers.authorization || '').trim());
if (!m) return false;
const decoded = Buffer.from(m[1], 'base64').toString('utf8');
const i = decoded.indexOf(':'); // first colon: passwords may contain ':'
if (i < 0) return false;
const userOk = safeEqual(decoded.slice(0, i), user); // evaluate both, no short-circuit
const passOk = safeEqual(decoded.slice(i + 1), pass);
return userOk && passOk;
}
- Header:
Authorization: Basic base64(username:password), scheme case-insensitive. - Compare username and password separately in constant time. In Python use
hmac.compare_digeston bytes (encode both sides;strraises on non-ASCII). - Return
401for a missing, malformed or mismatched header,200once handled. - The method is user-chosen: accept
POST,PUT,PATCH,GETandDELETEon the route, and handle an empty body (a GET has none). - Read the body raw (
express.raw({ type: '*/*' })) so any content type works, authenticate first, and parse JSON only after. Accept non-JSON bodies too: the marketer may write XML, form data or plain text with Liquid.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Request Bodies
Braze adds no event type. The body is whatever the message composes, for example a JSON object from key-value pairs:
{ "message_body": "Your order just arrived!", "user_id": "u_123" }
| Body style | What arrives |
|---|---|
| JSON key-value pairs | A JSON object with the keys and Liquid-rendered values you wrote |
| Raw text | Any format (XML, form data, plain text) with Liquid rendered |
| GET or DELETE | Usually no body; use the URL path and query |
Retries
| Your response | Braze behavior |
|---|---|
20x | Delivered |
30x, other 4xx (including 401) | Failed, not retried |
408, 429, 5xx | Retried, up to 5 attempts in total (up to 24 hours if unreachable) |
Each request has 120 seconds before it times out. Make handlers idempotent: a retry sends the same request again.
Environment Variables
BRAZE_WEBHOOK_USERNAME=your-braze-webhook-username # Settings > Credential Management
BRAZE_WEBHOOK_PASSWORD=your-braze-webhook-password
Local Development
Braze can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 braze --path /webhooks/braze
Use the printed URL as the webhook URL in Braze (a fixed hostname, HTTPS, to select a credential).
Resources
- references/overview.md - What Braze webhooks are, methods and request bodies
- references/setup.md - Create the credential and the webhook in Braze
- references/verification.md - Basic auth check details, limits, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests