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_digest on bytes (encode both sides; str raises on non-ASCII).
  • Return 401 for a missing, malformed or mismatched header, 200 once handled.
  • The method is user-chosen: accept POST, PUT, PATCH, GET and DELETE on 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 styleWhat arrives
JSON key-value pairsA JSON object with the keys and Liquid-rendered values you wrote
Raw textAny format (XML, form data, plain text) with Liquid rendered
GET or DELETEUsually no body; use the URL path and query

Retries

Your responseBraze behavior
20xDelivered
30x, other 4xx (including 401)Failed, not retried
408, 429, 5xxRetried, 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 4, 2026

View on GitHub →