or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

billing.mdcheckout.mdconfiguration.mdcore-resources.mdidentity.mdindex.mdissuing.mdradar.mdsubscriptions.mdtax.mdterminal.mdtreasury.mdwebhooks.md

webhooks.mddocs/

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.