# Webhooks

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.

## Webhook Verification and Construction

### Webhook Signature Verification

Stripe signs webhook payloads to ensure authenticity and prevent replay attacks:

```typescript { .api }
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
  );
}
```

### Test Webhook Generation

Generate test webhook signatures for development:

```typescript { .api }
// 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'
});
```

## Webhook Endpoint Management

### WebhookEndpoints

Manage webhook endpoints programmatically:

```typescript { .api }
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');
```

## Event Processing

### Events Resource

Retrieve and manage event objects:

```typescript { .api }
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
});
```

## Common Event Handlers

### Payment Events

Handle payment-related webhook events:

```typescript { .api }
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);
}
```

### Subscription Events

Handle subscription lifecycle events:

```typescript { .api }
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);
    }
  }
}
```

### Issuing Events

Handle card issuing events:

```typescript { .api }
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);
}
```

## V2 API Events

### ThinEvent Processing

Handle V2 API thin events for improved performance:

```typescript { .api }
// 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}`);
  }
}
```

## Webhook Best Practices

### Robust Event Processing

Implement idempotent, reliable webhook handlers:

```typescript { .api }
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()
    });
  }
}
```

### Error Handling and Retries

Implement proper error handling for webhook failures:

```typescript { .api }
// 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));
    }
  }
}
```

### Event Filtering and Routing

Route events to specialized handlers:

```typescript { .api }
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);
  }
}
```

## Testing Webhooks

### Local Development Setup

Test webhooks locally using Stripe CLI:

```bash
# 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_failed
```

### Unit Testing Webhook Handlers

Create comprehensive tests for webhook processing:

```typescript { .api }
// 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.