0
# Webhooks
1
2
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.
3
4
## Webhook Verification and Construction
5
6
### Webhook Signature Verification
7
8
Stripe signs webhook payloads to ensure authenticity and prevent replay attacks:
9
10
```typescript { .api }
11
interface WebhookEvent {
12
id: string;
13
object: 'event';
14
api_version: string;
15
created: number;
16
data: {
17
object: any;
18
previous_attributes?: any;
19
};
20
livemode: boolean;
21
pending_webhooks: number;
22
request: {
23
id: string | null;
24
idempotency_key: string | null;
25
};
26
type: string;
27
}
28
29
// Verify and construct webhook event
30
function handleWebhook(rawBody: string, signature: string, endpointSecret: string) {
31
let event: WebhookEvent;
32
33
try {
34
event = stripe.webhooks.constructEvent(rawBody, signature, endpointSecret);
35
} catch (err) {
36
console.log(`Webhook signature verification failed:`, err.message);
37
throw err;
38
}
39
40
return event;
41
}
42
43
// Async version for better performance
44
async function handleWebhookAsync(rawBody: string, signature: string, endpointSecret: string) {
45
let event: WebhookEvent;
46
47
try {
48
event = await stripe.webhooks.constructEventAsync(rawBody, signature, endpointSecret);
49
} catch (err) {
50
console.log(`Webhook signature verification failed:`, err.message);
51
throw err;
52
}
53
54
return event;
55
}
56
57
// Manual signature verification
58
function verifyWebhookSignature(
59
payload: string,
60
signature: string,
61
secret: string,
62
tolerance: number = 300
63
): boolean {
64
return stripe.webhooks.signature.verifyHeader(
65
payload,
66
signature,
67
secret,
68
tolerance
69
);
70
}
71
```
72
73
### Test Webhook Generation
74
75
Generate test webhook signatures for development:
76
77
```typescript { .api }
78
// Generate test webhook signature
79
const testSignature = stripe.webhooks.generateTestHeaderString({
80
timestamp: Math.floor(Date.now() / 1000),
81
payload: JSON.stringify(testEvent),
82
secret: 'whsec_test_secret'
83
});
84
85
// Async version
86
const testSignatureAsync = await stripe.webhooks.generateTestHeaderStringAsync({
87
timestamp: Math.floor(Date.now() / 1000),
88
payload: JSON.stringify(testEvent),
89
secret: 'whsec_test_secret',
90
scheme: 'v1'
91
});
92
```
93
94
## Webhook Endpoint Management
95
96
### WebhookEndpoints
97
98
Manage webhook endpoints programmatically:
99
100
```typescript { .api }
101
interface WebhookEndpoint {
102
id: string;
103
object: 'webhook_endpoint';
104
api_version?: string;
105
application?: string;
106
created: number;
107
description?: string;
108
enabled_events: string[];
109
livemode: boolean;
110
secret?: string;
111
status: 'enabled' | 'disabled';
112
url: string;
113
}
114
115
// Create webhook endpoint
116
const endpoint = await stripe.webhookEndpoints.create({
117
url: 'https://example.com/stripe/webhook',
118
enabled_events: [
119
'payment_intent.succeeded',
120
'payment_intent.payment_failed',
121
'invoice.payment_succeeded',
122
'invoice.payment_failed',
123
'customer.subscription.created',
124
'customer.subscription.updated',
125
'customer.subscription.deleted'
126
],
127
description: 'Main webhook endpoint for payments and subscriptions',
128
api_version: '2025-08-27.basil'
129
});
130
131
// Create endpoint for specific events
132
const paymentEndpoint = await stripe.webhookEndpoints.create({
133
url: 'https://example.com/payment-webhook',
134
enabled_events: [
135
'payment_intent.succeeded',
136
'payment_intent.payment_failed',
137
'payment_method.attached',
138
'charge.dispute.created'
139
],
140
description: 'Dedicated payment processing webhook'
141
});
142
143
// Retrieve webhook endpoint
144
const retrieved = await stripe.webhookEndpoints.retrieve('we_123');
145
146
// Update webhook endpoint
147
const updated = await stripe.webhookEndpoints.update('we_123', {
148
enabled_events: [
149
'payment_intent.succeeded',
150
'payment_intent.payment_failed',
151
'payment_intent.canceled',
152
'refund.created'
153
],
154
description: 'Updated payment webhook with refund events'
155
});
156
157
// List webhook endpoints
158
const endpoints = await stripe.webhookEndpoints.list();
159
160
// Delete webhook endpoint
161
const deleted = await stripe.webhookEndpoints.del('we_123');
162
```
163
164
## Event Processing
165
166
### Events Resource
167
168
Retrieve and manage event objects:
169
170
```typescript { .api }
171
interface Event {
172
id: string;
173
object: 'event';
174
api_version: string;
175
created: number;
176
data: {
177
object: any;
178
previous_attributes?: { [key: string]: any };
179
};
180
livemode: boolean;
181
pending_webhooks: number;
182
request?: {
183
id: string | null;
184
idempotency_key: string | null;
185
};
186
type: string;
187
}
188
189
// Retrieve specific event
190
const event = await stripe.events.retrieve('evt_123');
191
192
// List recent events
193
const events = await stripe.events.list({
194
limit: 100,
195
type: 'payment_intent.succeeded'
196
});
197
198
// List events by date range
199
const recentEvents = await stripe.events.list({
200
created: {
201
gte: Math.floor(Date.now() / 1000) - 86400 // Last 24 hours
202
},
203
limit: 50
204
});
205
206
// List events by type prefix
207
const paymentEvents = await stripe.events.list({
208
type: 'payment_intent.*',
209
limit: 100
210
});
211
```
212
213
## Common Event Handlers
214
215
### Payment Events
216
217
Handle payment-related webhook events:
218
219
```typescript { .api }
220
function handlePaymentEvents(event: WebhookEvent) {
221
switch (event.type) {
222
case 'payment_intent.succeeded':
223
const succeededPayment = event.data.object as Stripe.PaymentIntent;
224
return handlePaymentSucceeded(succeededPayment);
225
226
case 'payment_intent.payment_failed':
227
const failedPayment = event.data.object as Stripe.PaymentIntent;
228
return handlePaymentFailed(failedPayment);
229
230
case 'payment_intent.canceled':
231
const canceledPayment = event.data.object as Stripe.PaymentIntent;
232
return handlePaymentCanceled(canceledPayment);
233
234
case 'payment_intent.requires_action':
235
const actionRequiredPayment = event.data.object as Stripe.PaymentIntent;
236
return handlePaymentRequiresAction(actionRequiredPayment);
237
238
case 'payment_method.attached':
239
const attachedPaymentMethod = event.data.object as Stripe.PaymentMethod;
240
return handlePaymentMethodAttached(attachedPaymentMethod);
241
242
default:
243
console.log(`Unhandled payment event type: ${event.type}`);
244
}
245
}
246
247
async function handlePaymentSucceeded(paymentIntent: Stripe.PaymentIntent) {
248
// Update order status
249
await updateOrderStatus(paymentIntent.metadata?.order_id, 'paid');
250
251
// Send confirmation email
252
await sendPaymentConfirmation(paymentIntent.customer as string, paymentIntent.id);
253
254
// Update inventory if applicable
255
if (paymentIntent.metadata?.product_ids) {
256
await updateInventory(JSON.parse(paymentIntent.metadata.product_ids));
257
}
258
}
259
260
async function handlePaymentFailed(paymentIntent: Stripe.PaymentIntent) {
261
// Update order status
262
await updateOrderStatus(paymentIntent.metadata?.order_id, 'payment_failed');
263
264
// Send failure notification
265
await sendPaymentFailureNotification(paymentIntent.customer as string, paymentIntent.id);
266
267
// Log failure for analysis
268
await logPaymentFailure(paymentIntent);
269
}
270
```
271
272
### Subscription Events
273
274
Handle subscription lifecycle events:
275
276
```typescript { .api }
277
function handleSubscriptionEvents(event: WebhookEvent) {
278
switch (event.type) {
279
case 'customer.subscription.created':
280
const newSubscription = event.data.object as Stripe.Subscription;
281
return handleSubscriptionCreated(newSubscription);
282
283
case 'customer.subscription.updated':
284
const updatedSubscription = event.data.object as Stripe.Subscription;
285
const previousAttributes = event.data.previous_attributes;
286
return handleSubscriptionUpdated(updatedSubscription, previousAttributes);
287
288
case 'customer.subscription.deleted':
289
const deletedSubscription = event.data.object as Stripe.Subscription;
290
return handleSubscriptionDeleted(deletedSubscription);
291
292
case 'customer.subscription.trial_will_end':
293
const trialEndingSubscription = event.data.object as Stripe.Subscription;
294
return handleTrialWillEnd(trialEndingSubscription);
295
296
case 'invoice.payment_succeeded':
297
const paidInvoice = event.data.object as Stripe.Invoice;
298
return handleInvoicePaymentSucceeded(paidInvoice);
299
300
case 'invoice.payment_failed':
301
const failedInvoice = event.data.object as Stripe.Invoice;
302
return handleInvoicePaymentFailed(failedInvoice);
303
304
default:
305
console.log(`Unhandled subscription event type: ${event.type}`);
306
}
307
}
308
309
async function handleSubscriptionCreated(subscription: Stripe.Subscription) {
310
// Activate user account
311
await activateUserSubscription(subscription.customer as string, subscription.id);
312
313
// Send welcome email
314
await sendWelcomeEmail(subscription.customer as string, subscription);
315
316
// Set up user permissions
317
await updateUserPermissions(subscription.customer as string, subscription.items.data[0].price.id);
318
}
319
320
async function handleSubscriptionUpdated(
321
subscription: Stripe.Subscription,
322
previousAttributes: any
323
) {
324
// Check if plan changed
325
if (previousAttributes.items) {
326
const oldPriceId = previousAttributes.items.data[0].price.id;
327
const newPriceId = subscription.items.data[0].price.id;
328
329
if (oldPriceId !== newPriceId) {
330
await handlePlanChange(subscription.customer as string, oldPriceId, newPriceId);
331
}
332
}
333
334
// Check if status changed
335
if (previousAttributes.status && subscription.status !== previousAttributes.status) {
336
await handleStatusChange(subscription.customer as string, previousAttributes.status, subscription.status);
337
}
338
}
339
340
async function handleInvoicePaymentFailed(invoice: Stripe.Invoice) {
341
if (invoice.subscription) {
342
// Send payment failure notification
343
await sendPaymentFailureNotification(invoice.customer as string, invoice.id);
344
345
// Check if this is the final attempt
346
if (invoice.next_payment_attempt === null) {
347
await handleSubscriptionDelinquent(invoice.subscription as string);
348
}
349
}
350
}
351
```
352
353
### Issuing Events
354
355
Handle card issuing events:
356
357
```typescript { .api }
358
function handleIssuingEvents(event: WebhookEvent) {
359
switch (event.type) {
360
case 'issuing_authorization.request':
361
const authRequest = event.data.object as Stripe.Issuing.Authorization;
362
return handleAuthorizationRequest(authRequest);
363
364
case 'issuing_authorization.created':
365
const newAuth = event.data.object as Stripe.Issuing.Authorization;
366
return handleAuthorizationCreated(newAuth);
367
368
case 'issuing_transaction.created':
369
const newTransaction = event.data.object as Stripe.Issuing.Transaction;
370
return handleTransactionCreated(newTransaction);
371
372
case 'issuing_card.created':
373
const newCard = event.data.object as Stripe.Issuing.Card;
374
return handleCardCreated(newCard);
375
376
default:
377
console.log(`Unhandled issuing event type: ${event.type}`);
378
}
379
}
380
381
async function handleAuthorizationRequest(authorization: Stripe.Issuing.Authorization) {
382
// Implement real-time authorization logic
383
const shouldApprove = await evaluateAuthorizationRequest(authorization);
384
385
if (shouldApprove) {
386
await stripe.issuing.authorizations.approve(authorization.id);
387
} else {
388
await stripe.issuing.authorizations.decline(authorization.id, {
389
reason: 'spending_controls'
390
});
391
}
392
}
393
394
async function handleTransactionCreated(transaction: Stripe.Issuing.Transaction) {
395
// Update expense tracking
396
await recordExpense({
397
cardId: transaction.card,
398
amount: transaction.amount,
399
currency: transaction.currency,
400
merchant: transaction.merchant_data.name,
401
category: transaction.merchant_data.category,
402
date: new Date(transaction.created * 1000)
403
});
404
405
// Check for unusual spending patterns
406
await checkSpendingPatterns(transaction.card, transaction.amount);
407
}
408
```
409
410
## V2 API Events
411
412
### ThinEvent Processing
413
414
Handle V2 API thin events for improved performance:
415
416
```typescript { .api }
417
// Parse V2 thin event
418
function handleV2Webhook(rawBody: string, signature: string, secret: string) {
419
let thinEvent: Stripe.ThinEvent;
420
421
try {
422
thinEvent = stripe.parseThinEvent(rawBody, signature, secret);
423
} catch (err) {
424
console.log(`V2 webhook verification failed:`, err.message);
425
throw err;
426
}
427
428
return processThinEvent(thinEvent);
429
}
430
431
async function processThinEvent(thinEvent: Stripe.ThinEvent) {
432
// Fetch full event data only when needed
433
const fullEvent = await stripe.v2.core.events.retrieve(thinEvent.id);
434
435
switch (thinEvent.type) {
436
case 'v1.billing.meter_event_adjustment.created':
437
return handleMeterEventAdjustment(fullEvent.data);
438
439
case 'v1.billing.meter_event.created':
440
return handleMeterEvent(fullEvent.data);
441
442
default:
443
console.log(`Unhandled V2 event type: ${thinEvent.type}`);
444
}
445
}
446
```
447
448
## Webhook Best Practices
449
450
### Robust Event Processing
451
452
Implement idempotent, reliable webhook handlers:
453
454
```typescript { .api }
455
class WebhookProcessor {
456
private processedEvents = new Set<string>();
457
458
async processWebhook(event: WebhookEvent): Promise<void> {
459
// Implement idempotency
460
if (this.processedEvents.has(event.id)) {
461
console.log(`Event ${event.id} already processed`);
462
return;
463
}
464
465
try {
466
await this.handleEvent(event);
467
this.processedEvents.add(event.id);
468
} catch (error) {
469
console.error(`Error processing event ${event.id}:`, error);
470
throw error; // Let the webhook retry
471
}
472
}
473
474
private async handleEvent(event: WebhookEvent): Promise<void> {
475
// Use transaction for database operations
476
await this.database.transaction(async (trx) => {
477
switch (event.type) {
478
case 'payment_intent.succeeded':
479
await this.handlePaymentSuccess(event.data.object, trx);
480
break;
481
482
case 'customer.subscription.updated':
483
await this.handleSubscriptionUpdate(event.data.object, trx);
484
break;
485
486
default:
487
console.log(`Unhandled event type: ${event.type}`);
488
}
489
});
490
}
491
492
private async handlePaymentSuccess(paymentIntent: any, trx: any): Promise<void> {
493
// Update order status
494
await trx('orders')
495
.where('payment_intent_id', paymentIntent.id)
496
.update({ status: 'paid', updated_at: new Date() });
497
498
// Create fulfillment record
499
await trx('fulfillments').insert({
500
order_id: paymentIntent.metadata?.order_id,
501
payment_intent_id: paymentIntent.id,
502
status: 'pending',
503
created_at: new Date()
504
});
505
}
506
}
507
```
508
509
### Error Handling and Retries
510
511
Implement proper error handling for webhook failures:
512
513
```typescript { .api }
514
// Express.js webhook handler with error handling
515
app.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => {
516
const sig = req.headers['stripe-signature'];
517
let event: WebhookEvent;
518
519
try {
520
event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
521
} catch (err) {
522
console.log(`Webhook signature verification failed:`, err.message);
523
return res.status(400).send(`Webhook Error: ${err.message}`);
524
}
525
526
try {
527
await processWebhookEvent(event);
528
res.status(200).json({received: true});
529
} catch (error) {
530
console.error('Webhook processing failed:', error);
531
532
// Return 500 to trigger Stripe's retry mechanism
533
res.status(500).json({
534
error: 'Internal server error',
535
event_id: event.id
536
});
537
}
538
});
539
540
// Retry mechanism for failed webhook processing
541
async function processWebhookWithRetry(event: WebhookEvent, maxRetries: number = 3) {
542
let attempt = 0;
543
544
while (attempt < maxRetries) {
545
try {
546
await processWebhookEvent(event);
547
return; // Success
548
} catch (error) {
549
attempt++;
550
console.error(`Webhook processing attempt ${attempt} failed:`, error);
551
552
if (attempt >= maxRetries) {
553
// Log to dead letter queue or alerting system
554
await logFailedWebhook(event, error);
555
throw error;
556
}
557
558
// Exponential backoff
559
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
560
}
561
}
562
}
563
```
564
565
### Event Filtering and Routing
566
567
Route events to specialized handlers:
568
569
```typescript { .api }
570
class WebhookRouter {
571
private handlers = new Map<string, (event: WebhookEvent) => Promise<void>>();
572
573
constructor() {
574
// Register event handlers
575
this.handlers.set('payment_intent.*', this.handlePaymentEvents.bind(this));
576
this.handlers.set('customer.subscription.*', this.handleSubscriptionEvents.bind(this));
577
this.handlers.set('issuing_*', this.handleIssuingEvents.bind(this));
578
this.handlers.set('invoice.*', this.handleInvoiceEvents.bind(this));
579
}
580
581
async routeEvent(event: WebhookEvent): Promise<void> {
582
for (const [pattern, handler] of this.handlers) {
583
if (this.matchesPattern(event.type, pattern)) {
584
await handler(event);
585
return;
586
}
587
}
588
589
console.log(`No handler found for event type: ${event.type}`);
590
}
591
592
private matchesPattern(eventType: string, pattern: string): boolean {
593
if (pattern.endsWith('*')) {
594
return eventType.startsWith(pattern.slice(0, -1));
595
}
596
return eventType === pattern;
597
}
598
599
private async handlePaymentEvents(event: WebhookEvent): Promise<void> {
600
// Delegate to payment-specific handlers
601
const paymentProcessor = new PaymentEventProcessor();
602
await paymentProcessor.handle(event);
603
}
604
605
private async handleSubscriptionEvents(event: WebhookEvent): Promise<void> {
606
// Delegate to subscription-specific handlers
607
const subscriptionProcessor = new SubscriptionEventProcessor();
608
await subscriptionProcessor.handle(event);
609
}
610
}
611
```
612
613
## Testing Webhooks
614
615
### Local Development Setup
616
617
Test webhooks locally using Stripe CLI:
618
619
```bash
620
# Install Stripe CLI and login
621
stripe login
622
623
# Forward webhooks to local server
624
stripe listen --forward-to localhost:3000/webhook
625
626
# Trigger specific events for testing
627
stripe trigger payment_intent.succeeded
628
stripe trigger customer.subscription.created
629
stripe trigger invoice.payment_failed
630
```
631
632
### Unit Testing Webhook Handlers
633
634
Create comprehensive tests for webhook processing:
635
636
```typescript { .api }
637
// Jest test example
638
describe('Webhook Processing', () => {
639
const mockEvent = {
640
id: 'evt_test_123',
641
object: 'event',
642
type: 'payment_intent.succeeded',
643
data: {
644
object: {
645
id: 'pi_test_123',
646
amount: 2000,
647
currency: 'usd',
648
status: 'succeeded',
649
metadata: { order_id: 'order_123' }
650
}
651
},
652
created: Math.floor(Date.now() / 1000),
653
livemode: false,
654
api_version: '2025-08-27.basil',
655
pending_webhooks: 1,
656
request: { id: null, idempotency_key: null }
657
} as WebhookEvent;
658
659
it('should handle payment success correctly', async () => {
660
const processor = new WebhookProcessor();
661
662
await processor.processWebhook(mockEvent);
663
664
// Verify order status updated
665
const order = await getOrder('order_123');
666
expect(order.status).toBe('paid');
667
});
668
669
it('should be idempotent', async () => {
670
const processor = new WebhookProcessor();
671
672
// Process same event twice
673
await processor.processWebhook(mockEvent);
674
await processor.processWebhook(mockEvent);
675
676
// Verify only processed once
677
const orders = await getOrdersByPaymentIntent('pi_test_123');
678
expect(orders).toHaveLength(1);
679
});
680
});
681
```
682
683
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.