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 web applications that rely on real-time notifications from third-party services. Stripe, a popular payment processing platform, uses webhooks to notify your application about events like successful payments, failed charges, or subscription updates. In this guide, I'll walk you through building a robust Stripe webhook handler in Node.js, complete with verification, logging, and error handling.


Prerequisites

Before diving in, ensure you have the following:

  1. Node.js installed (v16 or later recommended).
  2. A Stripe account with a test API key.
  3. Express.js for handling HTTP requests.
  4. Stripe SDK installed (npm install stripe).

Setting Up Your Project

Initialize a new Node.js project and install the required dependencies:

mkdir stripe-webhook
cd stripe-webhook
npm init -y
npm install express stripe body-parser dotenv
Enter fullscreen mode Exit fullscreen mode

Create a .env file to store your Stripe secret key and webhook signing secret:

STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
PORT=3000
Enter fullscreen mode Exit fullscreen mode

Creating the Webhook Endpoint

First, set up an Express server to handle incoming Stripe webhook events:

const express = require('express');
const bodyParser = require('body-parser');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const app = express();
const port = process.env.PORT || 3000;

// Middleware to parse raw body for webhook verification
app.use(bodyParser.raw({ type: 'application/json' }));

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

  try {
    // Verify the webhook signature
    event = stripe.webhooks.constructEvent(
      req.body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    console.error('Webhook signature verification failed:', err.message);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // Handle the event
  switch (event.type) {
    case 'payment_intent.succeeded':
      const paymentIntent = event.data.object;
      console.log('PaymentIntent succeeded:', paymentIntent.id);
      break;

    case 'payment_intent.payment_failed':
      const failedPaymentIntent = event.data.object;
      console.log('PaymentIntent failed:', failedPaymentIntent.id);
      break;

    // Add more cases as needed
    default:
      console.log(`Unhandled event type: ${event.type}`);
  }

  // Send a response to Stripe
  res.json({ received: true });
});

app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});
Enter fullscreen mode Exit fullscreen mode

Verifying Webhook Signatures

A critical step in webhook implementation is verifying the authenticity of the incoming payload. Stripe signs each webhook request with a unique HMAC-SHA256 signature. The stripe.webhooks.constructEvent method ensures the payload is genuine by comparing the provided signature with the one generated using your webhook signing secret.

Key Points:

  1. Always use the raw body for verification.
  2. Never expose your webhook signing secret.
  3. Handle errors gracefully to avoid security vulnerabilities.

Handling Webhook Events

Stripe webhooks deliver a variety of event types (event.type). Your application should handle only the events relevant to your use case. In the example above, we handled payment_intent.succeeded and payment_intent.payment_failed. You can extend this to accommodate other events like customer.subscription.created or charge.refunded.

Here’s an enhanced example for subscription events:

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

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

  switch (event.type) {
    case 'payment_intent.succeeded':
      const paymentIntent = event.data.object;
      console.log('PaymentIntent succeeded:', paymentIntent.id);
      break;

    case 'payment_intent.payment_failed':
      const failedPaymentIntent = event.data.object;
      console.log('PaymentIntent failed:', failedPaymentIntent.id);
      break;

    case 'customer.subscription.created':
      const subscription = event.data.object;
      console.log('Subscription created:', subscription.id);
      break;

    case 'customer.subscription.deleted':
      const deletedSubscription = event.data.object;
      console.log('Subscription deleted:', deletedSubscription.id);
      break;

    default:
      console.log(`Unhandled event type: ${event.type}`);
  }

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

Adding Logging and Error Handling

Robust logging and error handling are essential for debugging and monitoring. Use libraries like winston or bunyan for structured logging. Here’s how to integrate winston:

npm install winston
Enter fullscreen mode Exit fullscreen mode
const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'webhook.log' }),
  ],
});

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

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

  logger.info(`Received event: ${event.type}`);

  switch (event.type) {
    case 'payment_intent.succeeded':
      const paymentIntent = event.data.object;
      logger.info('PaymentIntent succeeded:', paymentIntent.id);
      break;

    case 'payment_intent.payment_failed':
      const failedPaymentIntent = event.data.object;
      logger.error('PaymentIntent failed:', failedPaymentIntent.id);
      break;

    default:
      logger.warn(`Unhandled event type: ${event.type}`);
  }

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

Testing Your Webhook

1. Use Stripe CLI:

Install the Stripe CLI and forward webhooks to your local server:

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

2. Trigger Test Events:

Use the Stripe CLI or Dashboard to trigger test events:

stripe trigger payment_intent.succeeded
Enter fullscreen mode Exit fullscreen mode

Deploying to Production

When deploying your webhook handler to production:

  1. Use HTTPS to secure communication.
  2. Ensure your webhook signing secret is kept secure.
  3. Monitor logs for any unexpected behavior.
  4. Consider using a queue system (e.g., RabbitMQ or Redis) to handle events asynchronously.

Final Thoughts

Building a Stripe webhook handler in Node.js is straightforward but requires attention to detail, especially around security and error handling. By following this guide, you’ll have a robust system capable of handling real-time payment notifications and other Stripe events. Happy coding! 🚀


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