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

Webhooks are essential for modern payment processing systems, allowing real-time notifications about payment events. 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 application about events like:

  • Successful payments (payment_intent.succeeded)
  • Failed payments (payment_intent.payment_failed)
  • Subscription changes (customer.subscription.updated)

The critical components we'll implement:

  1. Webhook endpoint verification
  2. Event processing with idempotency
  3. Error handling and retry logic
  4. Queue integration for async processing

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

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 is paramount. We must verify the webhook signature to ensure requests come from Stripe:

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 with Idempotency

Stripe may send duplicate events. Implement idempotency using event IDs:

const processedEvents = new Set();

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

  try {
    switch (event.type) {
      case 'payment_intent.succeeded':
        await handlePaymentSuccess(event.data.object);
        break;
      case 'payment_intent.payment_failed':
        await handlePaymentFailure(event.data.object);
        break;
      // Add more event types as needed
      default:
        console.log(`Unhandled event type: ${event.type}`);
    }

    // Mark event as processed
    processedEvents.add(event.id);
  } catch (error) {
    console.error(`Error processing event ${event.id}:`, error);
    throw error; // Stripe will retry
  }
}
Enter fullscreen mode Exit fullscreen mode

Handling Payment Events

Implement business logic for payment events:

async function handlePaymentSuccess(paymentIntent) {
  console.log(`Payment succeeded for ${paymentIntent.amount} ${paymentIntent.currency}`);

  // Example: Update order status in database
  await updateOrderStatus(paymentIntent.metadata.orderId, 'paid');

  // Example: Send confirmation email
  await sendConfirmationEmail(paymentIntent.customer_email, {
    amount: paymentIntent.amount / 100,
    currency: paymentIntent.currency
  });
}

async function handlePaymentFailure(paymentIntent) {
  console.warn(`Payment failed for ${paymentIntent.id}`);

  // Example: Notify customer
  if (paymentIntent.last_payment_error?.message) {
    await sendPaymentFailedEmail(
      paymentIntent.customer_email,
      paymentIntent.last_payment_error.message
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Advanced: Queue Integration

For production workloads, use a queue system like Bull or SQS:

const Queue = require('bull');

const webhookQueue = new Queue('stripe-webhooks', {
  redis: process.env.REDIS_URL
});

// Process jobs in worker process
webhookQueue.process(async (job) => {
  const { event } = job.data;
  await processEvent(event);
});

// Modified webhook handler to enqueue events
async function handleStripeWebhook(req, res) {
  // ... verification code ...

  // Add to queue instead of processing directly
  await webhookQueue.add({
    event,
    timestamp: new Date()
  });

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

Error Handling and Retries

Implement proper error handling for production:

webhookQueue.on('failed', (job, err) => {
  console.error(`Job ${job.id} failed:`, err);

  if (job.attemptsMade < 3) {
    console.log(`Retrying job ${job.id}`);
    return true; // Will retry
  }

  // Permanent failure - alert and log
  alertAdmin(`Webhook processing failed after retries: ${err.message}`);
  logPermanentFailure(job.data.event);
});

// Exponential backoff configuration
webhookQueue.process(5, {
  attempts: 3,
  backoff: {
    type: 'exponential',
    delay: 5000
  }
});
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

Trigger test events:

stripe trigger payment_intent.succeeded
Enter fullscreen mode Exit fullscreen mode

Production Considerations

  1. Rate Limiting: Protect your endpoint from abuse
  2. Logging: Log all webhook events for audit purposes
  3. Monitoring: Set up alerts for failed webhooks
  4. Scaling: Use horizontal scaling for high-volume applications

Example rate limiting with Express:

const rateLimit = require('express-rate-limit');

const webhookLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // limit each IP to 100 requests per windowMs
});

app.post('/webhook', webhookLimiter, bodyParser.raw({type: 'application/json'}), handleStripeWebhook);
Enter fullscreen mode Exit fullscreen mode

Complete Example

Here's the complete implementation:

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

const app = express();
const processedEvents = new Set();
const webhookQueue = new Queue('stripe-webhooks', { redis: process.env.REDIS_URL });

// Queue processing
webhookQueue.process(async (job) => {
  const { event } = job.data;
  await processEvent(event);
});

webhookQueue.on('failed', (job, err) => {
  console.error(`Job ${job.id} failed:`, err);
  if (job.attemptsMade < 3) return true;
  alertAdmin(`Webhook processing failed: ${err.message}`);
});

async function processEvent(event) {
  if (processedEvents.has(event.id)) return;

  try {
    switch (event.type) {
      case 'payment_intent.succeeded':
        await handlePaymentSuccess(event.data.object);
        break;
      // Add other event types...
      default:
        console.log(`Unhandled event: ${event.type}`);
    }
    processedEvents.add(event.id);
  } catch (error) {
    console.error(`Event processing error:`, error);
    throw error;
  }
}

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 webhookQueue.add({ event });
      res.json({received: true});
    } catch (err) {
      res.status(400).send(`Webhook Error: ${err.message}`);
    }
  }
);

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

Conclusion

This implementation provides:

  • Secure webhook verification
  • Idempotent event processing
  • Queue-based async handling
  • Comprehensive error recovery
  • Production-ready architecture

Remember to:

  1. Keep your webhook secret secure
  2. Monitor your queue workers
  3. Test all event types thoroughly
  4. Implement proper logging

By following these patterns, you'll have a robust Stripe integration that can handle production workloads reliably.


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