# 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)

```javascript
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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/braze-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/braze-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/braze-webhooks/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:

```json
{ "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

```bash
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):

```bash
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](https://github.com/hookdeck/webhook-skills/blob/main/skills/braze-webhooks/references/overview.md) - What Braze webhooks are, methods and request bodies
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/braze-webhooks/references/setup.md) - Create the credential and the webhook in Braze
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/braze-webhooks/references/verification.md) - Basic auth check details, limits, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/braze-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests