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. In this technical deep dive, I'll walk through building a production-grade Stripe webhook handler in Node.js with proper security, error handling, and idempotency.

Understanding Stripe 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. Webhook endpoint verification
  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 Express server with the webhook endpoint:

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

const app = express();

// Use raw body for webhook verification
app.post('/webhook', bodyParser.raw({type: 'application/json'}), handleStripeWebhook);

app.listen(4242, () => console.log('Listening on port 4242'));
Enter fullscreen mode Exit fullscreen mode

Webhook Verification Security

The most critical security measure is verifying webhook signatures:

async function handleStripeWebhook(req, res) {
  const sig = req.headers['stripe-signature'];
  const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;

  let event;

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

  // Process the verified event
  await processEvent(event);

  res.json({received: true});
}
Enter fullscreen mode Exit fullscreen mode

Event Processing Pipeline

Implement a robust event processor with error handling:

const eventHandlers = {
  'payment_intent.succeeded': handleSuccessfulPayment,
  'payment_intent.payment_failed': handleFailedPayment,
  'invoice.payment_succeeded': handleSubscriptionPayment,
  // Add more event handlers as needed
};

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

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

  try {
    await handler(event);
  } catch (err) {
    console.error(`Error processing ${event.type}`, err);
    // Implement your retry logic here
    throw err;
  }
}
Enter fullscreen mode Exit fullscreen mode

Idempotency Implementation

To prevent duplicate processing:

const processedEvents = new Set();

async function processEvent(event) {
  // Check for duplicate events
  if (processedEvents.has(event.id)) {
    console.log(`Event ${event.id} already processed`);
    return;
  }

  // Process the event
  await eventHandlers[event.type](event);

  // Store the processed event ID
  processedEvents.add(event.id);

  // Optional: Implement TTL for memory cleanup
  setTimeout(() => processedEvents.delete(event.id), 24 * 60 * 60 * 1000);
}
Enter fullscreen mode Exit fullscreen mode

For production, replace the in-memory Set with Redis:

const redis = require('redis');
const client = redis.createClient();

async function isEventProcessed(eventId) {
  return new Promise((resolve) => {
    client.exists(`stripe_event:${eventId}`, (err, reply) => {
      resolve(reply === 1);
    });
  });
}

async function markEventProcessed(eventId) {
  client.setex(`stripe_event:${eventId}`, 86400, 'processed');
}
Enter fullscreen mode Exit fullscreen mode

Handling Payment Events

Example payment handler implementation:

async function handleSuccessfulPayment(event) {
  const paymentIntent = event.data.object;

  // Important business logic
  await fulfillOrder(paymentIntent.metadata.orderId);

  // Update your database
  await updatePaymentStatus(
    paymentIntent.id,
    'succeeded',
    paymentIntent.amount
  );

  console.log(`Payment for ${paymentIntent.amount} succeeded`);
}

async function fulfillOrder(orderId) {
  // Implement your order fulfillment logic
  // This might involve:
  // - Updating inventory
  // - Sending confirmation emails
  // - Triggering shipping processes
}
Enter fullscreen mode Exit fullscreen mode

Error Handling and Retries

Implement exponential backoff for failed events:

async function processEventWithRetry(event, attempt = 1) {
  try {
    await processEvent(event);
  } catch (err) {
    if (attempt >= 3) {
      console.error(`Final attempt failed for ${event.id}`);
      await storeFailedEvent(event);
      return;
    }

    const delay = Math.pow(2, attempt) * 1000;
    console.log(`Retrying ${event.id} in ${delay}ms`);

    await new Promise(resolve => setTimeout(resolve, delay));
    await processEventWithRetry(event, attempt + 1);
  }
}
Enter fullscreen mode Exit fullscreen mode

Testing Webhooks Locally

Use the Stripe CLI for local testing:

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

Then trigger test events:

stripe trigger payment_intent.succeeded
Enter fullscreen mode Exit fullscreen mode

Production Considerations

  1. Scale: Use queue systems (Bull, RabbitMQ) for high-volume webhooks
  2. Monitoring: Implement logging and alerts for failed webhooks
  3. Redundancy: Set up multiple webhook endpoints for critical events
  4. Versioning: Handle API version differences with Stripe's version header

Complete Webhook Handler Example

Here's the full implementation:

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

const app = express();
const redisClient = redis.createClient(process.env.REDIS_URL);

const eventHandlers = {
  'payment_intent.succeeded': handleSuccessfulPayment,
  'payment_intent.payment_failed': handleFailedPayment,
  'invoice.payment_succeeded': handleSubscriptionPayment
};

app.post('/webhook', 
  bodyParser.raw({type: 'application/json'}),
  async (req, res) => {
    const sig = req.headers['stripe-signature'];

    try {
      const event = stripe.webhooks.constructEvent(
        req.body,
        sig,
        process.env.STRIPE_WEBHOOK_SECRET
      );

      await processEventWithRetry(event);
      res.json({received: true});
    } catch (err) {
      console.error('Webhook error:', err);
      res.status(400).send(`Webhook Error: ${err.message}`);
    }
  }
);

async function processEventWithRetry(event, attempt = 1) {
  if (await isEventProcessed(event.id)) return;

  try {
    const handler = eventHandlers[event.type];
    if (handler) await handler(event);

    await markEventProcessed(event.id);
  } catch (err) {
    if (attempt >= 3) {
      await storeFailedEvent(event);
      return;
    }

    await new Promise(resolve => 
      setTimeout(resolve, Math.pow(2, attempt) * 1000));
    await processEventWithRetry(event, attempt + 1);
  }
}

// Start server
app.listen(4242, () => console.log('Webhook handler running'));
Enter fullscreen mode Exit fullscreen mode

Performance Optimization

For high-traffic systems:

  1. Implement event batching
  2. Use worker pools for CPU-intensive operations
  3. Consider database connection pooling
const { Worker, isMainThread, workerData } = require('worker_threads');

if (!isMainThread) {
  // Worker thread processing logic
  processEvent(workerData.event);
}

// In your webhook handler:
if (requiresHeavyProcessing(event)) {
  new Worker('./eventWorker.js', { workerData: { event } });
} else {
  processEvent(event);
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

This implementation provides:

  • Secure webhook verification
  • Reliable event processing
  • Idempotent operations
  • Comprehensive error handling
  • Scalability considerations

Remember to always:

  1. Verify webhook signatures
  2. Handle events idempotently
  3. Implement proper error handling
  4. Monitor your webhook processing

The complete code is production-ready and can be extended with additional event types and business logic as needed.


🚀 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)