Agent skill
Zuora Webhooks Skill
Receive and authenticate Zuora callout notifications (webhooks from Events and Notifications). Use when setting up a Zuora callout endpoint, verifying the HTTP Basic Authorization header, enabling Preemptive Authentication, debugging 401s, or handling callouts like a payment processed or invoice posted notification.
Install this skill
npx skills add hookdeck/webhook-skills --skill 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
Authorizationheader and only sends credentials after your server answers401with aWWW-Authenticatechallenge. 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
PaymentProcessedorInvoicePostedstyle callouts?
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 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_digeston bytes (encode both sides;strraises on non-ASCII). - Return
401for a missing, malformed or mismatched header,400for a body that isn't JSON, and200once handled. Zuora treats anything other than200as a failure. - Read the body raw (
express.raw({ type: '*/*' })) and authenticate before parsing it.
For complete handlers with tests, see examples/express/, examples/nextjs/, 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:
{"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
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):
npx hookdeck-cli listen 3000 zuora --path /webhooks/zuora
Use the printed URL as the callout template's Request URL.
Resources
- references/overview.md - What callouts are, headers, retries, body shape
- references/setup.md - Create the callout template with Preemptive Authentication
- references/verification.md - Basic auth check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests