# Zuora Webhooks

Zuora calls them **callouts**: event-based HTTP requests that Zuora sends to a URL you choose
when something happens in your tenant, such as a payment processed or an invoice posted. You
build them as callout templates in **Extension Studio > Events & Notifications**, and an active
notification definition has to select the template before anything is sent. This skill covers
callout notifications, not Workflow callouts.

Callouts are **not signed**. The sender is authenticated with HTTP Basic auth
(`Authorization: Basic base64(username:password)`) set on the template.

> **You must tick "Enable Preemptive Authentication" on the callout template.** By default
> Zuora sends the first request *without* an `Authorization` header and only sends credentials
> after your server answers `401` with a `WWW-Authenticate` challenge. That challenge flow does
> not work through Hookdeck, and it makes every verification check in this skill fail on the
> first request. With Preemptive Authentication on, Zuora puts the credentials in the first
> request, and the handler below can check them directly.

A static shared credential authenticates the **sender, not the body**, and gives **no replay
protection** (there is no signature and no timestamp). Use HTTPS only, treat the credential as a
secret, and rotate it on the template (and in your env vars) if it leaks.

## When to Use This Skill

* How do I receive Zuora callout notifications (webhooks)?
* How do I authenticate Zuora callouts with Basic auth?
* Why does my Zuora callout endpoint return 401 or never receive credentials?
* What does "Enable Preemptive Authentication" do?
* Does Zuora sign webhook payloads? (No: Basic auth, with an optional user-defined HMAC)
* How do I handle `PaymentProcessed` or `InvoicePosted` style callouts?

## 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 verifyZuoraRequest(headers, user = process.env.ZUORA_WEBHOOK_USERNAME,
                            pass = process.env.ZUORA_WEBHOOK_PASSWORD) {
  if (!user || !pass) return false;                      // never accept when unconfigured
  const [scheme, encoded, ...rest] = (headers.authorization || '').trim().split(/\s+/);
  if (scheme?.toLowerCase() !== 'basic' || !encoded || rest.length) return false;
  if (!/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)) return false;  // reject non-base64
  const decoded = Buffer.from(encoded, 'base64');
  const colon = decoded.indexOf(0x3a);                   // split on the FIRST colon
  if (colon === -1) return false;
  const userOk = safeEqual(decoded.subarray(0, colon), Buffer.from(user));
  const passOk = safeEqual(decoded.subarray(colon + 1), Buffer.from(pass));
  return userOk && passOk;                               // evaluate both, no short-circuit
}

```

* Header: `Authorization: Basic base64(username:password)`; the scheme is case-insensitive.
* Compare the username and the 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, `400` for a body that isn't JSON,
  and `200` once handled. Zuora treats anything other than `200` as a failure.
* Read the body raw (`express.raw({ type: '*/*' })`) and authenticate before parsing it.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/zuora-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/zuora-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/zuora-webhooks/examples/fastapi/).

## HTTP Methods and Body

The callout method is chosen on the template: `POST` (the default), `GET`, `PUT`, `PATCH` or
`DELETE`. The examples accept all five on `/webhooks/zuora` and handle an empty body (for
example `GET`, where merge fields arrive in the URL query instead).

Zuora adds **no event field**. The body is the merge fields you put in the template. The examples
assume an illustrative JSON body you write yourself:

```json
{"event": "PaymentProcessed", "accountId": "{{DataSource.Account.Id}}"}

```

| Illustrative `event` | Meaning |
| --- | --- |
| `PaymentProcessed` | Template on a payment-processed event |
| `InvoicePosted` | Template on an invoice-posted event |

These names are conventions of this skill, not Zuora-defined strings. Pick whatever your
templates send and update the dispatch. Zuora recommends putting little data in a callout (usually
an ID) and fetching the details from the Zuora API.

## Environment Variables

```bash
ZUORA_WEBHOOK_USERNAME=your-callout-username   # Callout template > Username
ZUORA_WEBHOOK_PASSWORD=your-callout-password   # Callout template > Password

```

## Local Development

Zuora can't reach `localhost`, and callout URLs must be `https://`. Tunnel with the Hookdeck CLI
(no account required):

```bash
npx hookdeck-cli listen 3000 zuora --path /webhooks/zuora

```

Use the printed URL as the callout template's Request URL.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/zuora-webhooks/references/overview.md) - What callouts are, headers, retries, body shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/zuora-webhooks/references/setup.md) - Create the callout template with Preemptive Authentication
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/zuora-webhooks/references/verification.md) - Basic auth check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/zuora-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests