Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
64194a8
local changes
cb-karthikp Jan 21, 2026
72bef8e
add typing for webhook handlers
cb-karthikp Jan 22, 2026
4b21b4f
correct eventtype import
cb-karthikp Jan 22, 2026
a19517d
refactor(webhooks): use object params for handle() method
cb-karthikp Feb 4, 2026
6a4ae59
chargebee.webhooks instance and createHandler()
cb-karthikp Feb 4, 2026
1a9ab34
utils update
cb-karthikp Feb 4, 2026
0517eb7
move default auth-validation to util
cb-karthikp Feb 4, 2026
a94576f
show warning when no-auth is setup
cb-karthikp Feb 4, 2026
f2f309f
add event entry field validation
cb-karthikp Feb 4, 2026
2e2d950
better error management
cb-karthikp Feb 4, 2026
4cab269
add deprecation message and strict-content type
cb-karthikp Feb 4, 2026
a59e3cc
update testcase async method
cb-karthikp Feb 4, 2026
5499246
add comment
cb-karthikp Feb 4, 2026
cda96a9
add comment
cb-karthikp Feb 4, 2026
fa5289f
update docs link url
cb-karthikp Feb 4, 2026
be78040
update readme
cb-karthikp Feb 4, 2026
f4470d6
Merge branch 'master' into webhook-event-handler
cb-karthikp Feb 5, 2026
14519c2
Merge branch 'master' into webhook-event-handler
cb-karthikp Feb 10, 2026
7070c87
Pass request/response context to webhook error listeners
cb-karthikp Feb 10, 2026
3cd3f32
add error class
cb-karthikp Feb 11, 2026
1a6d509
Update README.md
cb-karthikp Feb 11, 2026
54179ca
remove un-used error class
cb-karthikp Feb 11, 2026
bac31ee
export typings
cb-karthikp Feb 11, 2026
5b923f5
add error types to typings
cb-karthikp Feb 11, 2026
54ae408
add prefix for webhook error types
cb-karthikp Feb 11, 2026
29436f7
Version bump and CHANGELOG.md addition
cb-alish Feb 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
node_modules
test*
*js
cjs
esm
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
### v3.21.0 (2026-02-11)
* * *

### 🚀 New Feature

* Added webhook handler support. Refer to the [README.md](https://github.com/chargebee/chargebee-node/blob/master/README.md#handle-webhooks) for more details.

### v3.20.0 (2026-02-06)
* * *
### New Attributes:
Expand Down
273 changes: 250 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,253 @@ const chargebeeSiteEU = new Chargebee({
});
```

### Handle webhooks

Use the webhook handlers to parse and route webhook payloads from Chargebee with full TypeScript support.

#### Quick Start: Using the instance `webhooks` handler

The simplest way to handle webhooks is using the `webhooks` property on your initialized Chargebee client:

```typescript
import express from 'express';
import Chargebee, {
AuthenticationError,
PayloadValidationError,
PayloadParseError,
} from 'chargebee';

const chargebee = new Chargebee({
site: '{{site}}',
apiKey: '{{api-key}}',
});

const app = express();
app.use(express.json());

// ⚠️ Register listeners once at startup, not inside request handlers
chargebee.webhooks.on('subscription_created', async ({ event, response }) => {
console.log(`Subscription created: ${event.id}`);
const subscription = event.content.subscription;
console.log(`Customer: ${subscription.customer_id}`);
response?.status(200).send('OK');
});

chargebee.webhooks.on('error', (error, { response }) => {
if (error instanceof AuthenticationError) {
response?.status(401).send('Unauthorized');
} else if (error instanceof PayloadValidationError || error instanceof PayloadParseError) {
response?.status(400).send('Bad Request');
} else {
console.error('Webhook error:', error.message);
response?.status(500).send('Internal Server Error');
}
});

app.post('/chargebee/webhooks', async (req, res) => {
await chargebee.webhooks.handle({
body: req.body,
headers: req.headers,
request: req,
response: res,
});
});

app.listen(8080);
```

**Auto-configured Basic Auth:** The `webhooks` handler automatically configures Basic Auth validation if the following environment variables are set:

- `CHARGEBEE_WEBHOOK_USERNAME` - The expected username
- `CHARGEBEE_WEBHOOK_PASSWORD` - The expected password

When both are present, incoming webhook requests will be validated against these credentials.

#### Creating typed webhook handlers

For more control or multiple webhook endpoints, use `chargebee.webhooks.createHandler()`:

```typescript
import express, { Request, Response } from 'express';
import Chargebee, {
basicAuthValidator,
AuthenticationError,
PayloadValidationError,
PayloadParseError,
} from 'chargebee';

const chargebee = new Chargebee({
site: '{{site}}',
apiKey: '{{api-key}}',
});

const app = express();
app.use(express.json());

// Create a typed handler for Express
const handler = chargebee.webhooks.createHandler<Request, Response>();

// Optional: Add request validator (e.g., Basic Auth)
handler.requestValidator = basicAuthValidator((username, password) => {
return username === 'admin' && password === 'secret';
});

// ⚠️ Register event listeners once at startup, not inside request handlers
handler.on('subscription_created', async ({ event, response }) => {
console.log(`Subscription created: ${event.id}`);
const subscription = event.content.subscription;
console.log(`Customer: ${subscription.customer_id}`);
console.log(`Plan: ${subscription.plan_id}`);
response?.status(200).send('OK');
});

handler.on('payment_succeeded', async ({ event, response }) => {
console.log(`Payment succeeded: ${event.id}`);
const transaction = event.content.transaction;
const customer = event.content.customer;
console.log(`Amount: ${transaction.amount}, Customer: ${customer.email}`);
response?.status(200).send('OK');
});

handler.on('error', (error, { response }) => {
if (error instanceof AuthenticationError) {
response?.status(401).send('Unauthorized');
} else if (error instanceof PayloadValidationError || error instanceof PayloadParseError) {
response?.status(400).send('Bad Request');
} else {
console.error('Webhook error:', error.message);
response?.status(500).send('Internal Server Error');
}
});

app.post('/chargebee/webhooks', async (req, res) => {
await handler.handle({
body: req.body,
headers: req.headers,
request: req,
response: res,
});
});

app.listen(8080);
```

#### Low-level: Parse and handle events manually

For more control, you can parse webhook events manually:

```typescript
import express from 'express';
import Chargebee, { type WebhookEvent } from 'chargebee';

const app = express();
app.use(express.json());

app.post('/chargebee/webhooks', async (req, res) => {
try {
const event = req.body as WebhookEvent;

switch (event.event_type) {
case 'subscription_created':
// Access event content with proper typing
const subscription = event.content.subscription;
console.log('Subscription created:', subscription.id);
break;

case 'payment_succeeded':
const transaction = event.content.transaction;
console.log('Payment succeeded:', transaction.amount);
break;

default:
console.log('Unhandled event type:', event.event_type);
}

res.status(200).send('OK');
} catch (err) {
console.error('Error processing webhook:', err);
res.status(500).send('Error processing webhook');
}
});

app.listen(8080);
```

#### Responding to Webhooks

> ⚠️ **Important:** Always send an HTTP response from your webhook handlers. If you don't respond with a 2xx status, Chargebee will retry the webhook, potentially causing duplicate processing.

**Respond with 200** to acknowledge receipt:

```typescript
handler.on('subscription_created', async ({ event, response }) => {
await provisionAccess(event.content.subscription);
response?.status(200).json({ received: true });
});
```

**Respond with 5xx** so Chargebee retries on failure:

```typescript
handler.on('payment_succeeded', async ({ event, response }) => {
try {
await recordPayment(event.content.transaction);
response?.status(200).send('OK');
} catch (err) {
response?.status(500).json({ error: 'Processing failed' });
}
});
```

**Access request context** (headers, middleware data):

```typescript
handler.on('customer_created', async ({ event, request, response }) => {
const tenantId = (request as any)?.tenant?.id;
await createCustomerForTenant(tenantId, event.content.customer);
response?.status(200).send('OK');
});
```

#### Handling Unhandled Events and Errors

The webhook handler provides specific error classes for different failure scenarios:

- **`AuthenticationError`** - Authentication failed (missing/invalid credentials)
- **`PayloadValidationError`** - Invalid webhook payload structure (missing required fields)
- **`PayloadParseError`** - Failed to parse JSON body

```typescript
import {
AuthenticationError,
PayloadValidationError,
PayloadParseError,
} from 'chargebee';

// Handle events without registered listeners
handler.on('unhandled_event', async ({ event, response }) => {
console.log(`Unhandled: ${event.event_type}`);
response?.status(200).send('OK');
});

// Handle errors with appropriate HTTP status codes
handler.on('error', (error, { response }) => {
if (error instanceof AuthenticationError) {
console.error('Authentication failed:', error.message);
response?.status(401).send('Unauthorized');
} else if (error instanceof PayloadValidationError) {
console.error('Invalid payload:', error.message);
response?.status(400).send('Bad Request');
} else if (error instanceof PayloadParseError) {
console.error('Failed to parse JSON:', error.message);
response?.status(400).send('Bad Request');
} else {
console.error('Unknown error:', error.message);
response?.status(500).send('Internal Server Error');
}
});
```

### Processing Webhooks - API Version Check

An attribute `api_version` is added to the [Event](https://apidocs.chargebee.com/docs/api/events) resource, which indicates the API version based on which the event content is structured. In your webhook servers, ensure this `api_version` is the same as the [API version](https://apidocs.chargebee.com/docs/api#versions) used by your webhook server's client library.
Expand Down Expand Up @@ -227,35 +474,15 @@ To improve type safety and gain better autocompletion when working with webhooks
import Chargebee, { WebhookEventType, WebhookEvent } from "chargebee";

const result = await chargebeeInstance.event.retrieve("{event-id}");
const subscriptionActivatedEvent: WebhookEvent<typeof WebhookEventType.SubscriptionActivated> = result.event;
const subscriptionActivatedEvent: WebhookEvent<WebhookEventType.SubscriptionActivated> = result.event;
const subscription = subscriptionActivatedEvent.content.subscription;
```

You can also use `WebhookEventType` in switch statements for runtime event handling:

```ts
import { WebhookEventType, WebhookEvent } from "chargebee";

function handleWebhook(event: WebhookEvent) {
switch (event.event_type) {
case WebhookEventType.SubscriptionCreated:
console.log("Subscription created:", event.content.subscription?.id);
break;
case WebhookEventType.PaymentSucceeded:
console.log("Payment succeeded:", event.content.transaction?.id);
break;
default:
console.log("Unhandled event:", event.event_type);
}
}
```

#### Notes

* `WebhookEvent<T>` provides type hinting for the event payload, making it easier to work with specific event structures.
* Use `WebhookEventType` to specify the exact event type (e.g., `SubscriptionCreated`, `InvoiceGenerated`, etc.).
* `WebhookEventType` is available at runtime, so you can use it in switch statements and comparisons.
* `WebhookContentType` is deprecated but still available for backward compatibility.
* Use the `WebhookEventType` to specify the exact event type (e.g., `SubscriptionCreated`, `InvoiceGenerated`, etc.).
* This approach ensures you get proper IntelliSense and compile-time checks when accessing event fields.

### Custom HTTP Client

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.20.0
3.21.0
Loading