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 handling asynchronous events in payment processing systems. In this deep dive, I'll show you how to build 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:

  1. Endpoint Verification: Validating Stripe signatures
  2. Event Processing: Handling different event types
  3. Idempotency: Preventing duplicate processing
  4. Error Handling: Managing failed deliveries

Initial Setup

First, install required packages:

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

Basic server setup:

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

const app = express();

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

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

Signature Verification

Security is paramount. Always verify the webhook signature:

function verifyStripeSignature(req) {
  const signature = req.headers['stripe-signature'];
  const signingSecret = process.env.STRIPE_WEBHOOK_SECRET;

  try {
    return stripe.webhooks.constructEvent(
      req.body,
      signature,
      signingSecret
    );
  } catch (err) {
    console.error('āš ļø Webhook signature verification failed', err);
    throw new Error('Invalid signature');
  }
}
Enter fullscreen mode Exit fullscreen mode

Event Processing Architecture

Implement a robust event processor with proper error handling:

const eventHandlers = {
  'payment_intent.succeeded': handleSuccessfulPayment,
  'charge.failed': handleFailedCharge,
  'invoice.payment_succeeded': handleSubscriptionPayment,
  // Add more event types as needed
};

async function handleStripeWebhook(req, res) {
  let event;

  try {
    event = verifyStripeSignature(req);
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  const handler = eventHandlers[event.type];

  if (handler) {
    try {
      await handler(event);
      res.json({received: true});
    } catch (err) {
      console.error(`Error processing ${event.type}`, err);
      res.status(500).send('Internal Server Error');
    }
  } else {
    res.status(400).send(`Unhandled event type: ${event.type}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Implementing Idempotency

Stripe may retry failed webhook deliveries. Use idempotency keys:

const processedEvents = new Set();

async function processEventWithIdempotency(event) {
  if (processedEvents.has(event.id)) {
    console.log(`Event ${event.id} already processed`);
    return;
  }

  // Process the event
  await handleEventLogic(event);

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

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

Database Integration

For production, use a database to track processed events:

const { MongoClient } = require('mongodb');

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

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

Handling Specific Events

Example implementation for payment success:

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

  // Business logic
  await fulfillOrder(paymentIntent.metadata.orderId);
  await sendConfirmationEmail(paymentIntent.customer);

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

async function fulfillOrder(orderId) {
  // Update your database, trigger shipping, etc.
  console.log(`Fulfilling order ${orderId}`);
}
Enter fullscreen mode Exit fullscreen mode

Error Handling and Retries

Implement exponential backoff for failed operations:

async function withRetry(fn, maxAttempts = 3) {
  let attempt = 0;

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

      const delay = Math.pow(2, attempt) * 1000;
      console.log(`Retrying in ${delay}ms...`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}
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. Queue Processing: Use Redis or RabbitMQ for high volume
  3. Monitoring: Track successful/failed webhooks
  4. Logging: Record all incoming events for debugging

Example queue implementation:

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

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

webhookQueue.process(async job => {
  const { event } = job.data;
  const handler = eventHandlers[event.type];
  if (handler) await handler(event);
});

// In your webhook handler:
await webhookQueue.add({ event });
Enter fullscreen mode Exit fullscreen mode

Complete Example

Here's the full implementation:

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 webhookQueue = new Queue('stripe_webhooks', {
  redis: process.env.REDIS_URL
});

const eventHandlers = {
  'payment_intent.succeeded': async (event) => {
    const paymentIntent = event.data.object;
    await fulfillOrder(paymentIntent.metadata.orderId);
  },
  // Other event handlers...
};

function verifyStripeSignature(req) {
  const signature = req.headers['stripe-signature'];
  return stripe.webhooks.constructEvent(
    req.body,
    signature,
    process.env.STRIPE_WEBHOOK_SECRET
  );
}

app.post('/webhook', 
  bodyParser.raw({type: 'application/json'}),
  async (req, res) => {
    let event;

    try {
      event = verifyStripeSignature(req);
      await webhookQueue.add({ event });
      res.json({received: true});
    } catch (err) {
      console.error('Webhook error:', err);
      res.status(400).send(`Webhook Error: ${err.message}`);
    }
  }
);

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

Final Thoughts

Building a robust Stripe webhook handler requires attention to:

  • Security (signature verification)
  • Reliability (idempotency, retries)
  • Scalability (queue processing)
  • Maintainability (clear event handling structure)

This implementation gives you a production-ready foundation that you can extend with your specific business logic. Remember to monitor your webhook processing and set up alerts for failed deliveries.


šŸš€ 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 (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.