CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-stripe

Stripe API wrapper for Node.js providing comprehensive payment processing, subscription management, and financial services integration.

Pending
Overview
Eval results
Files

webhooks.mddocs/

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:

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:

// 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:

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:

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:

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:

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:

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:

// 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:

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:

// 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:

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:

# 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:

// 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

docs

billing.md

checkout.md

configuration.md

core-resources.md

identity.md

index.md

issuing.md

radar.md

subscriptions.md

tax.md

terminal.md

treasury.md

webhooks.md

tile.json