Guide to Chargebee Webhooks: Features and Best Practices
Chargebee webhooks notify your application about subscription and billing activity: a subscription is created or cancelled, a payment succeeds or fails, an invoice is generated. If you're building on Chargebee Billing, webhooks are how you react to these events without polling.
This guide covers how Chargebee webhooks work, the events you'll handle, why there's no signature to verify (and what to do instead), and the best practices for production.
What are Chargebee webhooks?
Chargebee webhooks are JSON POSTs delivered to a URL you configure. There is no HMAC signature and no signature header of any kind: Chargebee's docs state that HMAC signing is not supported. Any integration claiming an hmac-signature for Chargebee is wrong. Authenticity instead rests on non-cryptographic, opt-in layers: HTTP Basic Auth credentials on the webhook URL, an older secret-in-URL pattern, and region-specific sending IP ranges for firewall allowlisting. Because nothing is signed, there's no timestamp, no replay window, and no raw-body concern.
Chargebee webhook features
| Feature | Details |
|---|---|
| Configuration | Settings > Webhooks (up to 5 endpoints) |
| Verification | None cryptographic. Optional Basic Auth (off by default), secret-in-URL, IP allowlist |
| Event field | Top-level event_type (snake_case, not dotted) |
| Payload | Shape varies by the site's api_version (v1/v2) and chargebee_response_schema_type |
| Card data | Masked by default |
| Retries | 7 attempts at 2min, 6min, 30min, 1hr, 5hr, 1day, 2days; endpoint must return 2xx |
| SDK | npm chargebee, pip chargebee (no verification helper, because nothing is signed) |
Common events
Chargebee event names are snake_case in the top-level event_type field (not dotted):
| Event | Fires when |
|---|---|
subscription_created / subscription_activated | A subscription is created or activated |
subscription_changed / subscription_cancelled / subscription_renewed | A subscription changes, cancels, or renews |
invoice_generated | An invoice is generated |
payment_succeeded / payment_failed | A payment resolves |
customer_created / customer_deleted | A customer is created or deleted |
payment_source_expiring | A payment source is about to expire |
The envelope carries id, occurred_at, source, event_type, api_version, and content.
Setting up Chargebee webhooks
In the Chargebee dashboard, go to Settings > Webhooks > Add Webhook, set a name and HTTPS URL, and choose the events. To add Basic Auth, toggle "Protect webhook URL with basic authentication" on and set a username and password. This toggle is off by default, so an unconfigured Chargebee webhook has no verification at all. Chargebee also publishes region-specific (US / EU / AU) sending IP ranges you can allowlist.
Securing Chargebee webhooks
There's no signature, so the strongest option Chargebee offers is HTTP Basic Auth. Configure it on the endpoint, then check the Authorization header on every request with a constant-time comparison. Serve the endpoint over HTTPS and treat the credentials as secrets. Layer the IP allowlist on top.
const crypto = require("crypto");
const USER = process.env.CHARGEBEE_WEBHOOK_USERNAME;
const PASS = process.env.CHARGEBEE_WEBHOOK_PASSWORD;
function safeEqual(a, b) {
const ba = Buffer.from(a || "");
const bb = Buffer.from(b || "");
return ba.length === bb.length && crypto.timingSafeEqual(ba, bb);
}
function verifyBasicAuth(header) {
if (!header || !header.startsWith("Basic ")) return false;
const decoded = Buffer.from(header.slice(6), "base64").toString("utf8");
const i = decoded.indexOf(":"); // split on the FIRST colon (passwords may contain ':')
return safeEqual(decoded.slice(0, i), USER) && safeEqual(decoded.slice(i + 1), PASS);
}
app.post("/webhooks/chargebee", express.json(), (req, res) => {
if (!verifyBasicAuth(req.headers["authorization"])) return res.sendStatus(401);
res.sendStatus(200); // acknowledge fast (2xx)
processQueue.add(req.body); // branch on event_type, async
});
The same check in Python:
import base64
import hmac
import os
USER = os.environ["CHARGEBEE_WEBHOOK_USERNAME"]
PASS = os.environ["CHARGEBEE_WEBHOOK_PASSWORD"]
def verify_basic_auth(header: str) -> bool:
if not header or not header.startswith("Basic "):
return False
decoded = base64.b64decode(header[6:]).decode("utf-8")
user, _, password = decoded.partition(":") # split on the first colon
return hmac.compare_digest(user, USER) and hmac.compare_digest(password, PASS)
Chargebee webhook limitations and pain points
There's no signature, and auth is off by default
The Problem: Chargebee doesn't sign webhooks, and Basic Auth is opt-in and off by default. An unconfigured endpoint accepts anything, so you can't prove a request came from Chargebee.
Why It Happens: HMAC signing isn't supported; Basic Auth is a toggle you must enable.
Workarounds:
- Enable Basic Auth, verify it in constant time, serve HTTPS only, and allowlist Chargebee's region IP ranges.
How Hookdeck Can Help: Hookdeck sits in front of Chargebee and adds verification, filtering, and delivery observability the source can't provide, turning an unsigned webhook into a monitored, replayable stream.
The payload shape varies per site
The Problem: The body shape depends on the site's api_version (v1 vs v2) and its chargebee_response_schema_type. Two Chargebee sites can emit structurally different bodies for the same event_type.
Why It Happens: Chargebee renders the payload according to per-site settings.
Workarounds:
- Pin to a known
api_version, and read defensively rather than assuming one shape.
How Hookdeck Can Help: Hookdeck's transformations can normalize varying payloads into a consistent shape before your app sees them.
Credentials can leak
The Problem: Basic Auth credentials (and the older secret-in-URL) can end up in logs and proxies, and the URL secret is the weaker of the two.
Why It Happens: The credential travels with each request; the URL secret sits in the query string.
Workarounds:
- Prefer Basic Auth over the URL secret, keep credentials out of logs, and rotate on exposure.
How Hookdeck Can Help: Hookdeck can hold the check centrally, so credentials aren't spread across every consumer's logs.
Duplicates and out-of-order delivery
The Problem: Chargebee retries up to 7 times and can deliver out of order, so the same event can arrive more than once.
Why It Happens: At-least-once delivery with a fixed retry schedule.
Workarounds:
- Dedupe on the event
idand make handlers idempotent.
How Hookdeck Can Help: Hookdeck deduplicates deliveries at the edge. See our guide to webhook idempotency.
Best practices
Enable and verify Basic Auth
Turn on Basic Auth (it's off by default), and compare the Authorization header in constant time.
Allowlist the region IP ranges
Restrict to Chargebee's US / EU / AU sending IP ranges as defense in depth.
Read the payload defensively
The shape varies by api_version and chargebee_response_schema_type, so don't assume one structure.
Acknowledge fast, process asynchronously
Return 2xx quickly and defer work to a queue; dedupe on id. See why to process webhooks asynchronously.
Make Chargebee webhooks production-ready
Hookdeck adds verification, deduplication, and observability to Chargebee's unsigned webhooks
Conclusion
Chargebee webhooks have no HMAC signature: authenticity rests on opt-in HTTP Basic Auth (off by default), an older secret-in-URL option, and region IP allowlisting. Enable Basic Auth and verify it in constant time, allowlist the sending IPs, read the api_version-dependent payload defensively, and dedupe on id. Because the source offers no verification or observability of its own, it's a strong case for a gateway in front.
Hookdeck adds verification, deduplication, and delivery observability to Chargebee's unsigned webhooks, so your app processes a trustworthy, monitored stream.
Get started with Hookdeck for free and handle Chargebee webhooks reliably in minutes.