DEV Community

Apollo
Apollo

Posted on

How I built a Stripe Webhook in Node.js (Full Guide)

How I Built a Stripe Webhook in Node.js (Full Technical Guide)

Webhooks are essential for handling asynchronous events in payment processing systems. Here's my deep dive into building a production-grade Stripe webhook handler in Node.js with proper security, error handling, and architecture.

Understanding the Webhook Architecture

Stripe webhooks use HTTP POST requests to notify your server about events like:

  • Successful payments (payment_intent.succeeded)
  • Failed charges (charge.failed)
  • Subscription changes (customer.subscription.updated)

The critical components we'll implement:

  1. Endpoint verification using Stripe signatures
  2. Event processing pipeline
  3. Idempotency handling
  4. Error recovery mechanisms

Initial Setup

First, install required dependencies:

npm install stripe express body-parser crypto
Enter fullscreen mode Exit fullscreen mode

Configure your Stripe instance:

const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const express = require('express');
const bodyParser = require('body-parser');
const crypto = require('crypto');

const app = express();
app.use(bodyParser.raw({type: 'application/json'}));
Enter fullscreen mode Exit fullscreen mode

Webhook Endpoint Implementation

Here's the core webhook handler with signature verification:

const WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET;

app.post('/webhook', async (req, res) => {
  const sig = req.headers['stripe-signature'];

  let event;

  try {
    event = stripe.webhooks.constructEvent(
      req.body,
      sig,
      WEBHOOK_SECRET
    );
  } catch (err) {
    console.error(`Webhook signature verification failed: ${err.message}`);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // Process the event
  try {
    await handleStripeEvent(event);
    res.json({received: true});
  } catch (err) {
    console.error(`Event processing failed: ${err.message}`);
    res.status(500).send(`Processing Error: ${err.message}`);
  }
});
Enter fullscreen mode Exit fullscreen mode

Event Processing Pipeline

Implement a robust event handler with proper error management:

const eventHandlers = {
  'payment_intent.succeeded': async (paymentIntent) => {
    // Business logic for successful payment
    console.log(`Payment succeeded: ${paymentIntent.id}`);
    await fulfillOrder(paymentIntent);
  },
  'payment_intent.payment_failed': async (paymentIntent) => {
    console.log(`Payment failed: ${paymentIntent.id}`);
    await handleFailedPayment(paymentIntent);
  },
  // Add more event handlers as needed
};

async function handleStripeEvent(event) {
  const handler = eventHandlers[event.type];

  if (!handler) {
    console.log(`Unhandled event type: ${event.type}`);
    return;
  }

  try {
    await handler(event.data.object);
  } catch (err) {
    console.error(`Handler for ${event.type} failed:`, err);
    throw err; // Rethrow for proper HTTP response
  }
}
Enter fullscreen mode Exit fullscreen mode

Idempotency Implementation

Critical for preventing duplicate processing:

const processedEvents = new Set();

async function handleStripeEvent(event) {
  // Check for duplicate events
  if (processedEvents.has(event.id)) {
    console.log(`Duplicate event detected: ${event.id}`);
    return;
  }

  processedEvents.add(event.id);

  // Clean up old events (simple in-memory implementation)
  // In production, use Redis with TTL
  if (processedEvents.size > 1000) {
    const oldest = Array.from(processedEvents).slice(-1000);
    processedEvents.clear();
    oldest.forEach(id => processedEvents.add(id));
  }

  // Rest of event handling...
}
Enter fullscreen mode Exit fullscreen mode

Production-Grade Enhancements

1. Database Integration

// Using MongoDB as example
const { MongoClient } = require('mongodb');

async function isEventProcessed(eventId) {
  const client = await MongoClient.connect(process.env.MONGODB_URI);
  const db = client.db('payments');
  const count = await db.collection('processed_events')
    .countDocuments({ eventId });
  await client.close();
  return count > 0;
}

async function markEventProcessed(eventId) {
  const client = await MongoClient.connect(process.env.MONGODB_URI);
  const db = client.db('payments');
  await db.collection('processed_events')
    .insertOne({ eventId, processedAt: new Date() });
  await client.close();
}
Enter fullscreen mode Exit fullscreen mode

2. Retry Logic with Exponential Backoff

async function withRetry(fn, maxRetries = 3, baseDelay = 1000) {
  let attempt = 0;

  while (attempt < maxRetries) {
    try {
      return await fn();
    } catch (err) {
      attempt++;
      if (attempt >= maxRetries) throw err;

      const delay = baseDelay * Math.pow(2, attempt);
      console.log(`Retry ${attempt} in ${delay}ms`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Queue Integration for Heavy Processing

const { Worker } = require('bull');

// Using Bull queue system
const paymentQueue = new Queue('stripe_webhooks', {
  redis: {
    host: process.env.REDIS_HOST,
    port: process.env.REDIS_PORT
  }
});

// In your webhook handler:
paymentQueue.add(event.type, {
  eventId: event.id,
  data: event.data.object
});

// Worker process
const worker = new Worker('stripe_webhooks', async job => {
  const { eventId, data } = job.data;
  await handleStripeEvent({ id: eventId, data });
});
Enter fullscreen mode Exit fullscreen mode

Testing Your Webhook

Use the Stripe CLI for local testing:

stripe listen --forward-to localhost:3000/webhook
Enter fullscreen mode Exit fullscreen mode

Then trigger test events:

stripe trigger payment_intent.succeeded
Enter fullscreen mode Exit fullscreen mode

Security Best Practices

  1. Always verify webhook signatures
  2. Use HTTPS for your webhook endpoint
  3. Implement rate limiting
  4. Keep your Stripe API keys secure
  5. Validate all incoming data

Monitoring and Alerting

// Simple logging wrapper
async function withMonitoring(fn, eventType) {
  const start = Date.now();
  try {
    const result = await fn();
    console.log({
      event: eventType,
      status: 'success',
      duration: Date.now() - start
    });
    return result;
  } catch (err) {
    console.error({
      event: eventType,
      status: 'failed',
      duration: Date.now() - start,
      error: err.message
    });
    // Integrate with your alerting system here
    sendAlert(`Webhook processing failed for ${eventType}`);
    throw err;
  }
}
Enter fullscreen mode Exit fullscreen mode

Final Production Checklist

  1. [ ] Signature verification implemented
  2. [ ] Idempotency handling in place
  3. [ ] Error recovery mechanisms
  4. [ ] Proper logging and monitoring
  5. [ ] Load testing completed
  6. [ ] Alerting configured
  7. [ ] Documentation for ops team

This implementation provides a solid foundation for handling Stripe webhooks in Node.js. The architecture is designed to be scalable, secure, and maintainable for production workloads. Remember to adapt the code to your specific business requirements and infrastructure.


🚀 Stop Writing Boilerplate Prompts

If you want to skip the setup and code 10x faster with complete AI architecture patterns, grab my Senior React Developer AI Cookbook ($19). It includes Server Action prompt libraries, UI component generation loops, and hydration debugging strategies.

Browse all 10+ developer products at the Apollo AI Store | Or snipe Solana tokens free via @ApolloSniper_Bot.

Top comments (0)