Stripe API wrapper for Node.js providing comprehensive payment processing, subscription management, and financial services integration.
—
Stripe webhooks provide real-time event notifications for changes in your Stripe account. This system enables reliable event-driven architecture by delivering HTTP POST requests to your application when significant events occur, such as successful payments, failed charges, or subscription updates.
Stripe signs webhook payloads to ensure authenticity and prevent replay attacks:
interface WebhookEvent {
id: string;
object: 'event';
api_version: string;
created: number;
data: {
object: any;
previous_attributes?: any;
};
livemode: boolean;
pending_webhooks: number;
request: {
id: string | null;
idempotency_key: string | null;
};
type: string;
}
// Verify and construct webhook event
function handleWebhook(rawBody: string, signature: string, endpointSecret: string) {
let event: WebhookEvent;
try {
event = stripe.webhooks.constructEvent(rawBody, signature, endpointSecret);
} catch (err) {
console.log(`Webhook signature verification failed:`, err.message);
throw err;
}
return event;
}
// Async version for better performance
async function handleWebhookAsync(rawBody: string, signature: string, endpointSecret: string) {
let event: WebhookEvent;
try {
event = await stripe.webhooks.constructEventAsync(rawBody, signature, endpointSecret);
} catch (err) {
console.log(`Webhook signature verification failed:`, err.message);
throw err;
}
return event;
}
// Manual signature verification
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string,
tolerance: number = 300
): boolean {
return stripe.webhooks.signature.verifyHeader(
payload,
signature,
secret,
tolerance
);
}Generate test webhook signatures for development:
// Generate test webhook signature
const testSignature = stripe.webhooks.generateTestHeaderString({
timestamp: Math.floor(Date.now() / 1000),
payload: JSON.stringify(testEvent),
secret: 'whsec_test_secret'
});
// Async version
const testSignatureAsync = await stripe.webhooks.generateTestHeaderStringAsync({
timestamp: Math.floor(Date.now() / 1000),
payload: JSON.stringify(testEvent),
secret: 'whsec_test_secret',
scheme: 'v1'
});Manage webhook endpoints programmatically:
interface WebhookEndpoint {
id: string;
object: 'webhook_endpoint';
api_version?: string;
application?: string;
created: number;
description?: string;
enabled_events: string[];
livemode: boolean;
secret?: string;
status: 'enabled' | 'disabled';
url: string;
}
// Create webhook endpoint
const endpoint = await stripe.webhookEndpoints.create({
url: 'https://example.com/stripe/webhook',
enabled_events: [
'payment_intent.succeeded',
'payment_intent.payment_failed',
'invoice.payment_succeeded',
'invoice.payment_failed',
'customer.subscription.created',
'customer.subscription.updated',
'customer.subscription.deleted'
],
description: 'Main webhook endpoint for payments and subscriptions',
api_version: '2025-08-27.basil'
});
// Create endpoint for specific events
const paymentEndpoint = await stripe.webhookEndpoints.create({
url: 'https://example.com/payment-webhook',
enabled_events: [
'payment_intent.succeeded',
'payment_intent.payment_failed',
'payment_method.attached',
'charge.dispute.created'
],
description: 'Dedicated payment processing webhook'
});
// Retrieve webhook endpoint
const retrieved = await stripe.webhookEndpoints.retrieve('we_123');
// Update webhook endpoint
const updated = await stripe.webhookEndpoints.update('we_123', {
enabled_events: [
'payment_intent.succeeded',
'payment_intent.payment_failed',
'payment_intent.canceled',
'refund.created'
],
description: 'Updated payment webhook with refund events'
});
// List webhook endpoints
const endpoints = await stripe.webhookEndpoints.list();
// Delete webhook endpoint
const deleted = await stripe.webhookEndpoints.del('we_123');Retrieve and manage event objects:
interface Event {
id: string;
object: 'event';
api_version: string;
created: number;
data: {
object: any;
previous_attributes?: { [key: string]: any };
};
livemode: boolean;
pending_webhooks: number;
request?: {
id: string | null;
idempotency_key: string | null;
};
type: string;
}
// Retrieve specific event
const event = await stripe.events.retrieve('evt_123');
// List recent events
const events = await stripe.events.list({
limit: 100,
type: 'payment_intent.succeeded'
});
// List events by date range
const recentEvents = await stripe.events.list({
created: {
gte: Math.floor(Date.now() / 1000) - 86400 // Last 24 hours
},
limit: 50
});
// List events by type prefix
const paymentEvents = await stripe.events.list({
type: 'payment_intent.*',
limit: 100
});Handle payment-related webhook events:
function handlePaymentEvents(event: WebhookEvent) {
switch (event.type) {
case 'payment_intent.succeeded':
const succeededPayment = event.data.object as Stripe.PaymentIntent;
return handlePaymentSucceeded(succeededPayment);
case 'payment_intent.payment_failed':
const failedPayment = event.data.object as Stripe.PaymentIntent;
return handlePaymentFailed(failedPayment);
case 'payment_intent.canceled':
const canceledPayment = event.data.object as Stripe.PaymentIntent;
return handlePaymentCanceled(canceledPayment);
case 'payment_intent.requires_action':
const actionRequiredPayment = event.data.object as Stripe.PaymentIntent;
return handlePaymentRequiresAction(actionRequiredPayment);
case 'payment_method.attached':
const attachedPaymentMethod = event.data.object as Stripe.PaymentMethod;
return handlePaymentMethodAttached(attachedPaymentMethod);
default:
console.log(`Unhandled payment event type: ${event.type}`);
}
}
async function handlePaymentSucceeded(paymentIntent: Stripe.PaymentIntent) {
// Update order status
await updateOrderStatus(paymentIntent.metadata?.order_id, 'paid');
// Send confirmation email
await sendPaymentConfirmation(paymentIntent.customer as string, paymentIntent.id);
// Update inventory if applicable
if (paymentIntent.metadata?.product_ids) {
await updateInventory(JSON.parse(paymentIntent.metadata.product_ids));
}
}
async function handlePaymentFailed(paymentIntent: Stripe.PaymentIntent) {
// Update order status
await updateOrderStatus(paymentIntent.metadata?.order_id, 'payment_failed');
// Send failure notification
await sendPaymentFailureNotification(paymentIntent.customer as string, paymentIntent.id);
// Log failure for analysis
await logPaymentFailure(paymentIntent);
}Handle subscription lifecycle events:
function handleSubscriptionEvents(event: WebhookEvent) {
switch (event.type) {
case 'customer.subscription.created':
const newSubscription = event.data.object as Stripe.Subscription;
return handleSubscriptionCreated(newSubscription);
case 'customer.subscription.updated':
const updatedSubscription = event.data.object as Stripe.Subscription;
const previousAttributes = event.data.previous_attributes;
return handleSubscriptionUpdated(updatedSubscription, previousAttributes);
case 'customer.subscription.deleted':
const deletedSubscription = event.data.object as Stripe.Subscription;
return handleSubscriptionDeleted(deletedSubscription);
case 'customer.subscription.trial_will_end':
const trialEndingSubscription = event.data.object as Stripe.Subscription;
return handleTrialWillEnd(trialEndingSubscription);
case 'invoice.payment_succeeded':
const paidInvoice = event.data.object as Stripe.Invoice;
return handleInvoicePaymentSucceeded(paidInvoice);
case 'invoice.payment_failed':
const failedInvoice = event.data.object as Stripe.Invoice;
return handleInvoicePaymentFailed(failedInvoice);
default:
console.log(`Unhandled subscription event type: ${event.type}`);
}
}
async function handleSubscriptionCreated(subscription: Stripe.Subscription) {
// Activate user account
await activateUserSubscription(subscription.customer as string, subscription.id);
// Send welcome email
await sendWelcomeEmail(subscription.customer as string, subscription);
// Set up user permissions
await updateUserPermissions(subscription.customer as string, subscription.items.data[0].price.id);
}
async function handleSubscriptionUpdated(
subscription: Stripe.Subscription,
previousAttributes: any
) {
// Check if plan changed
if (previousAttributes.items) {
const oldPriceId = previousAttributes.items.data[0].price.id;
const newPriceId = subscription.items.data[0].price.id;
if (oldPriceId !== newPriceId) {
await handlePlanChange(subscription.customer as string, oldPriceId, newPriceId);
}
}
// Check if status changed
if (previousAttributes.status && subscription.status !== previousAttributes.status) {
await handleStatusChange(subscription.customer as string, previousAttributes.status, subscription.status);
}
}
async function handleInvoicePaymentFailed(invoice: Stripe.Invoice) {
if (invoice.subscription) {
// Send payment failure notification
await sendPaymentFailureNotification(invoice.customer as string, invoice.id);
// Check if this is the final attempt
if (invoice.next_payment_attempt === null) {
await handleSubscriptionDelinquent(invoice.subscription as string);
}
}
}Handle card issuing events:
function handleIssuingEvents(event: WebhookEvent) {
switch (event.type) {
case 'issuing_authorization.request':
const authRequest = event.data.object as Stripe.Issuing.Authorization;
return handleAuthorizationRequest(authRequest);
case 'issuing_authorization.created':
const newAuth = event.data.object as Stripe.Issuing.Authorization;
return handleAuthorizationCreated(newAuth);
case 'issuing_transaction.created':
const newTransaction = event.data.object as Stripe.Issuing.Transaction;
return handleTransactionCreated(newTransaction);
case 'issuing_card.created':
const newCard = event.data.object as Stripe.Issuing.Card;
return handleCardCreated(newCard);
default:
console.log(`Unhandled issuing event type: ${event.type}`);
}
}
async function handleAuthorizationRequest(authorization: Stripe.Issuing.Authorization) {
// Implement real-time authorization logic
const shouldApprove = await evaluateAuthorizationRequest(authorization);
if (shouldApprove) {
await stripe.issuing.authorizations.approve(authorization.id);
} else {
await stripe.issuing.authorizations.decline(authorization.id, {
reason: 'spending_controls'
});
}
}
async function handleTransactionCreated(transaction: Stripe.Issuing.Transaction) {
// Update expense tracking
await recordExpense({
cardId: transaction.card,
amount: transaction.amount,
currency: transaction.currency,
merchant: transaction.merchant_data.name,
category: transaction.merchant_data.category,
date: new Date(transaction.created * 1000)
});
// Check for unusual spending patterns
await checkSpendingPatterns(transaction.card, transaction.amount);
}Handle V2 API thin events for improved performance:
// Parse V2 thin event
function handleV2Webhook(rawBody: string, signature: string, secret: string) {
let thinEvent: Stripe.ThinEvent;
try {
thinEvent = stripe.parseThinEvent(rawBody, signature, secret);
} catch (err) {
console.log(`V2 webhook verification failed:`, err.message);
throw err;
}
return processThinEvent(thinEvent);
}
async function processThinEvent(thinEvent: Stripe.ThinEvent) {
// Fetch full event data only when needed
const fullEvent = await stripe.v2.core.events.retrieve(thinEvent.id);
switch (thinEvent.type) {
case 'v1.billing.meter_event_adjustment.created':
return handleMeterEventAdjustment(fullEvent.data);
case 'v1.billing.meter_event.created':
return handleMeterEvent(fullEvent.data);
default:
console.log(`Unhandled V2 event type: ${thinEvent.type}`);
}
}Implement idempotent, reliable webhook handlers:
class WebhookProcessor {
private processedEvents = new Set<string>();
async processWebhook(event: WebhookEvent): Promise<void> {
// Implement idempotency
if (this.processedEvents.has(event.id)) {
console.log(`Event ${event.id} already processed`);
return;
}
try {
await this.handleEvent(event);
this.processedEvents.add(event.id);
} catch (error) {
console.error(`Error processing event ${event.id}:`, error);
throw error; // Let the webhook retry
}
}
private async handleEvent(event: WebhookEvent): Promise<void> {
// Use transaction for database operations
await this.database.transaction(async (trx) => {
switch (event.type) {
case 'payment_intent.succeeded':
await this.handlePaymentSuccess(event.data.object, trx);
break;
case 'customer.subscription.updated':
await this.handleSubscriptionUpdate(event.data.object, trx);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
});
}
private async handlePaymentSuccess(paymentIntent: any, trx: any): Promise<void> {
// Update order status
await trx('orders')
.where('payment_intent_id', paymentIntent.id)
.update({ status: 'paid', updated_at: new Date() });
// Create fulfillment record
await trx('fulfillments').insert({
order_id: paymentIntent.metadata?.order_id,
payment_intent_id: paymentIntent.id,
status: 'pending',
created_at: new Date()
});
}
}Implement proper error handling for webhook failures:
// Express.js webhook handler with error handling
app.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => {
const sig = req.headers['stripe-signature'];
let event: WebhookEvent;
try {
event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
} catch (err) {
console.log(`Webhook signature verification failed:`, err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
try {
await processWebhookEvent(event);
res.status(200).json({received: true});
} catch (error) {
console.error('Webhook processing failed:', error);
// Return 500 to trigger Stripe's retry mechanism
res.status(500).json({
error: 'Internal server error',
event_id: event.id
});
}
});
// Retry mechanism for failed webhook processing
async function processWebhookWithRetry(event: WebhookEvent, maxRetries: number = 3) {
let attempt = 0;
while (attempt < maxRetries) {
try {
await processWebhookEvent(event);
return; // Success
} catch (error) {
attempt++;
console.error(`Webhook processing attempt ${attempt} failed:`, error);
if (attempt >= maxRetries) {
// Log to dead letter queue or alerting system
await logFailedWebhook(event, error);
throw error;
}
// Exponential backoff
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
}
}
}Route events to specialized handlers:
class WebhookRouter {
private handlers = new Map<string, (event: WebhookEvent) => Promise<void>>();
constructor() {
// Register event handlers
this.handlers.set('payment_intent.*', this.handlePaymentEvents.bind(this));
this.handlers.set('customer.subscription.*', this.handleSubscriptionEvents.bind(this));
this.handlers.set('issuing_*', this.handleIssuingEvents.bind(this));
this.handlers.set('invoice.*', this.handleInvoiceEvents.bind(this));
}
async routeEvent(event: WebhookEvent): Promise<void> {
for (const [pattern, handler] of this.handlers) {
if (this.matchesPattern(event.type, pattern)) {
await handler(event);
return;
}
}
console.log(`No handler found for event type: ${event.type}`);
}
private matchesPattern(eventType: string, pattern: string): boolean {
if (pattern.endsWith('*')) {
return eventType.startsWith(pattern.slice(0, -1));
}
return eventType === pattern;
}
private async handlePaymentEvents(event: WebhookEvent): Promise<void> {
// Delegate to payment-specific handlers
const paymentProcessor = new PaymentEventProcessor();
await paymentProcessor.handle(event);
}
private async handleSubscriptionEvents(event: WebhookEvent): Promise<void> {
// Delegate to subscription-specific handlers
const subscriptionProcessor = new SubscriptionEventProcessor();
await subscriptionProcessor.handle(event);
}
}Test webhooks locally using Stripe CLI:
# Install Stripe CLI and login
stripe login
# Forward webhooks to local server
stripe listen --forward-to localhost:3000/webhook
# Trigger specific events for testing
stripe trigger payment_intent.succeeded
stripe trigger customer.subscription.created
stripe trigger invoice.payment_failedCreate comprehensive tests for webhook processing:
// Jest test example
describe('Webhook Processing', () => {
const mockEvent = {
id: 'evt_test_123',
object: 'event',
type: 'payment_intent.succeeded',
data: {
object: {
id: 'pi_test_123',
amount: 2000,
currency: 'usd',
status: 'succeeded',
metadata: { order_id: 'order_123' }
}
},
created: Math.floor(Date.now() / 1000),
livemode: false,
api_version: '2025-08-27.basil',
pending_webhooks: 1,
request: { id: null, idempotency_key: null }
} as WebhookEvent;
it('should handle payment success correctly', async () => {
const processor = new WebhookProcessor();
await processor.processWebhook(mockEvent);
// Verify order status updated
const order = await getOrder('order_123');
expect(order.status).toBe('paid');
});
it('should be idempotent', async () => {
const processor = new WebhookProcessor();
// Process same event twice
await processor.processWebhook(mockEvent);
await processor.processWebhook(mockEvent);
// Verify only processed once
const orders = await getOrdersByPaymentIntent('pi_test_123');
expect(orders).toHaveLength(1);
});
});Webhooks provide the foundation for building robust, event-driven integrations with Stripe, enabling real-time synchronization between Stripe events and your application's business logic.
Install with Tessl CLI
npx tessl i tessl/npm-stripe