/stripe-guides

How to handle webhooks in Stripe API?

Learn how to securely set up, verify, and handle Stripe webhooks in your app, with best practices for event processing, idempotency, error handling, and testing.

Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

Book a free consultation

How to handle webhooks in Stripe API?

How to Handle Webhooks in Stripe API

 

Introduction

 

Stripe webhooks are a powerful way to receive notifications about events happening in your Stripe account. This tutorial will guide you through setting up and handling Stripe webhooks securely and efficiently.

 

Step 1: Understanding Stripe Webhooks

 

Webhooks are HTTP callbacks that Stripe sends to your server when events occur in your account, such as successful payments, failed charges, or subscription updates.

Benefits of using webhooks:

  • Real-time notifications of Stripe events
  • Automatic synchronization between Stripe and your database
  • Ability to trigger business logic based on payment events

 

Step 2: Set Up a Webhook Endpoint in Your Application

 

First, create an endpoint in your application to receive webhook events. Here's an example using Express.js:


const express = require('express');
const app = express();

// This is necessary to parse the JSON payload from Stripe
app.use(
  express.json({
    // Use raw body for signature verification
    verify: (req, res, buf) => {
      req.rawBody = buf;
    }
  })
);

// Webhook endpoint
app.post('/stripe-webhook', (req, res) => {
  // We'll handle the webhook payload in the next steps
  console.log('Received webhook!');
  
  // Always return a 200 response quickly to acknowledge receipt
  res.status(200).send('Webhook received');
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

 

Step 3: Register Your Webhook in the Stripe Dashboard

 

  1. Log in to your Stripe Dashboard
  2. Navigate to Developers > Webhooks
  3. Click "Add endpoint"
  4. Enter your webhook URL (e.g., https://yourdomain.com/stripe-webhook)
  5. Select the events you want to receive (or choose "Select all events")
  6. Click "Add endpoint" to save

Note that for development, you can use tools like ngrok to expose your local server to the internet.

 

Step 4: Install Stripe Library

 

Install the Stripe library for your programming language:


# For Node.js
npm install stripe

# For Python
pip install stripe

# For PHP
composer require stripe/stripe-php

# For Ruby
gem install stripe

 

Step 5: Set Up Webhook Signature Verification

 

Verify the webhook signature to ensure it's genuinely from Stripe:


const stripe = require('stripe')('sk_test_YOUR_SECRET_KEY');
const endpointSecret = 'whsec_YOUR_WEBHOOK_SIGNING_SECRET';

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

  try {
    // Verify the event came from Stripe
    event = stripe.webhooks.constructEvent(req.rawBody, sig, endpointSecret);
  } catch (err) {
    // Invalid signature
    console.error(`Webhook signature verification failed: ${err.message}`);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // Handle the event
  console.log('Webhook verified:', event.id);
  
  // Return a response to acknowledge receipt of the event
  res.status(200).json({received: true});
});

The webhook signing secret can be found in your Stripe Dashboard under the webhook settings.

 

Step 6: Handle Different Webhook Events

 

Process different types of webhook events based on the event.type property:


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

  try {
    event = stripe.webhooks.constructEvent(req.rawBody, sig, endpointSecret);
  } catch (err) {
    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 for ${paymentIntent.amount} was successful!`);
      // Then define and call a function to handle the successful payment intent
      await handleSuccessfulPayment(paymentIntent);
      break;
      
    case 'payment\_method.attached':
      const paymentMethod = event.data.object;
      // Handle payment method attached
      break;
      
    case 'invoice.paid':
      const invoice = event.data.object;
      // Handle paid invoice
      await handlePaidInvoice(invoice);
      break;
      
    case 'customer.subscription.created':
      const subscription = event.data.object;
      // Handle subscription creation
      await handleSubscriptionCreated(subscription);
      break;
      
    // ... handle other event types
      
    default:
      console.log(`Unhandled event type ${event.type}`);
  }

  // Return a 200 response to acknowledge receipt of the event
  res.status(200).json({received: true});
});

 

Step 7: Implement Event Handlers

 

Create specific functions to handle each event type:


async function handleSuccessfulPayment(paymentIntent) {
  try {
    // Update order status in your database
    await updateOrderStatus(paymentIntent.metadata.orderId, 'paid');
    
    // Send confirmation email to the customer
    await sendPaymentConfirmationEmail(paymentIntent.receipt\_email);
    
    // Additional business logic
    console.log(`Payment for order ${paymentIntent.metadata.orderId} processed successfully`);
  } catch (error) {
    console.error('Error handling successful payment:', error);
    // Consider implementing retry logic or alerting
  }
}

async function handlePaidInvoice(invoice) {
  try {
    // Update subscription status
    await updateSubscriptionStatus(invoice.subscription, 'active');
    
    // Record payment in your system
    await recordPayment({
      customerId: invoice.customer,
      amount: invoice.amount\_paid,
      invoiceId: invoice.id,
      date: new Date(invoice.created \* 1000)
    });
    
    console.log(`Invoice ${invoice.id} paid successfully`);
  } catch (error) {
    console.error('Error handling paid invoice:', error);
  }
}

async function handleSubscriptionCreated(subscription) {
  try {
    // Store subscription details in your database
    await saveSubscription({
      id: subscription.id,
      customerId: subscription.customer,
      status: subscription.status,
      planId: subscription.plan.id,
      currentPeriodEnd: new Date(subscription.current_period_end \* 1000)
    });
    
    console.log(`Subscription ${subscription.id} created successfully`);
  } catch (error) {
    console.error('Error handling subscription creation:', error);
  }
}

 

Step 8: Test Your Webhook Implementation

 

Stripe provides several ways to test webhooks:

  1. Use the Stripe CLI for local testing:

# Install Stripe CLI (example for macOS with Homebrew)
brew install stripe/stripe-cli/stripe

# Login to your Stripe account
stripe login

# Forward events to your local server
stripe listen --forward-to localhost:3000/stripe-webhook
  1. Trigger test events using the Stripe CLI:

# Trigger a specific event
stripe trigger payment\_intent.succeeded
  1. Use the webhook tester in the Stripe Dashboard under Developers > Webhooks > Test in the webhook details.

 

Step 9: Implement Idempotency

 

Webhooks may be delivered more than once, so implement idempotency to handle duplicate events:


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

  try {
    event = stripe.webhooks.constructEvent(req.rawBody, sig, endpointSecret);
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // Check if we've processed this event already
  const eventId = event.id;
  const existingEvent = await checkIfEventProcessed(eventId);
  
  if (existingEvent) {
    // Event already processed, return success
    return res.status(200).json({received: true});
  }
  
  // Process the event based on its type
  try {
    switch (event.type) {
      case 'payment\_intent.succeeded':
        // Handle event
        break;
      // Other cases
    }
    
    // Mark event as processed
    await markEventAsProcessed(eventId);
    
    // Return success
    res.status(200).json({received: true});
  } catch (error) {
    console.error(`Error processing webhook: ${error.message}`);
    // Return a 500 so Stripe will retry the webhook
    res.status(500).send(`Server Error: ${error.message}`);
  }
});

// Helper functions for idempotency
async function checkIfEventProcessed(eventId) {
  // Check your database if this event ID has been processed
  // Return true if processed, false otherwise
  // Example:
  // return await db.collection('processedEvents').findOne({ eventId });
}

async function markEventAsProcessed(eventId) {
  // Store the event ID in your database
  // Example:
  // await db.collection('processedEvents').insertOne({ 
  //   eventId, 
  //   processedAt: new Date() 
  // });
}

 

Step 10: Handle Webhook Failures and Retries

 

Stripe will retry webhook deliveries on a decreasing frequency over the next 72 hours if your endpoint:

  • Returns a non-2xx HTTP status code
  • Times out (takes over 10 seconds to respond)
  • Has network failures

Implement a robust error handling strategy:


app.post('/stripe-webhook', async (req, res) => {
  // Previous verification code...
  
  try {
    // Process the event
    await processEvent(event);
    
    // Return success
    res.status(200).json({received: true});
  } catch (error) {
    console.error(`Error processing webhook: ${error.message}`);
    
    // Determine if we should ask Stripe to retry
    if (isRetryableError(error)) {
      // Return a 500 so Stripe will retry the webhook
      return res.status(500).send(`Server Error: ${error.message}`);
    } else {
      // For non-retryable errors, still acknowledge receipt
      // but log the error for investigation
      console.error('Non-retryable webhook error:', error);
      return res.status(200).json({received: true, error: error.message});
    }
  }
});

function isRetryableError(error) {
  // Examples of retryable errors:
  // - Database connection issues
  // - Temporary network problems
  // - Rate limiting from third-party APIs
  
  // Examples of non-retryable errors:
  // - Invalid data format
  // - Business logic errors
  
  // Implement your logic to determine if this error should trigger a retry
  return error.isRetryable === true || 
         error.code === 'DATABASE_CONNECTION_ERROR' ||
         error.message.includes('timeout');
}

 

Step 11: Monitor and Log Webhook Events

 

Implement logging to track and debug webhook events:


const winston = require('winston');

// Configure logger
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  defaultMeta: { service: 'webhook-service' },
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

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

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

  // Log the received event
  logger.info('Webhook received', { 
    eventId: event.id, 
    eventType: event.type, 
    objectId: event.data.object.id 
  });

  // Process event...
  
  // Log success
  logger.info('Webhook processed successfully', { eventId: event.id });
  res.status(200).json({received: true});
});

 

Step 12: Implement Webhook Security Best Practices

 

Secure your webhook implementation with these best practices:

  1. Always verify webhook signatures:

// Never skip this step
event = stripe.webhooks.constructEvent(req.rawBody, sig, endpointSecret);
  1. Store your webhook signing secret securely (use environment variables):

// Load from environment variables
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
  1. Use HTTPS for your webhook endpoint

  2. Implement proper access controls for your webhook endpoint

  3. Rotate your webhook secrets periodically:


// Steps to rotate webhook secrets:
// 1. Create a new webhook endpoint in Stripe with the same URL and events
// 2. Update your application to accept both the old and new secrets
const oldEndpointSecret = process.env.STRIPE_WEBHOOK_SECRET\_OLD;
const newEndpointSecret = process.env.STRIPE_WEBHOOK_SECRET\_NEW;

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

  try {
    // Try with the new secret first
    event = stripe.webhooks.constructEvent(req.rawBody, sig, newEndpointSecret);
  } catch (err1) {
    try {
      // If the new secret fails, try with the old secret
      event = stripe.webhooks.constructEvent(req.rawBody, sig, oldEndpointSecret);
      // Log that we used the old secret (for monitoring the transition)
      console.log('Used old webhook secret for event', event.id);
    } catch (err2) {
      // Both secrets failed
      console.error(`Webhook signature verification failed: ${err2.message}`);
      return res.status(400).send(`Webhook Error: ${err2.message}`);
    }
  }

  // Process the event...
  res.status(200).json({received: true});
});

 

Conclusion

 

Properly implementing Stripe webhooks allows your application to react to payment events in real-time, keeping your system in sync with Stripe's data. Following these steps will help you build a robust, secure, and reliable webhook handler for Stripe events.

Remember these key points:

  • Always verify webhook signatures to ensure security
  • Implement idempotency to handle duplicate events
  • Use proper error handling and logging
  • Test thoroughly before deploying to production
  • Monitor your webhook endpoint performance and success rates

Want to explore opportunities to work with us?

Connect with our team to unlock the full potential of no-code solutions with a no-commitment consultation!

Book a Free Consultation

Client trust and success are our top priorities

When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.

Rapid Dev was an exceptional project management organization and the best development collaborators I've had the pleasure of working with. They do complex work on extremely fast timelines and effectively manage the testing and pre-launch process to deliver the best possible product. I'm extremely impressed with their execution ability.

CPO, Praction - Arkady Sokolov

May 2, 2023

Working with Matt was comparable to having another co-founder on the team, but without the commitment or cost. He has a strategic mindset and willing to change the scope of the project in real time based on the needs of the client. A true strategic thought partner!

Co-Founder, Arc - Donald Muir

Dec 27, 2022

Rapid Dev are 10/10, excellent communicators - the best I've ever encountered in the tech dev space. They always go the extra mile, they genuinely care, they respond quickly, they're flexible, adaptable and their enthusiasm is amazing.

Co-CEO, Grantify - Mat Westergreen-Thorne

Oct 15, 2022

Rapid Dev is an excellent developer for no-code and low-code solutions.
We’ve had great success since launching the platform in November 2023. In a few months, we’ve gained over 1,000 new active users. We’ve also secured several dozen bookings on the platform and seen about 70% new user month-over-month growth since the launch.

Co-Founder, Church Real Estate Marketplace - Emmanuel Brown

May 1, 2024 

Matt’s dedication to executing our vision and his commitment to the project deadline were impressive. 
This was such a specific project, and Matt really delivered. We worked with a really fast turnaround, and he always delivered. The site was a perfect prop for us!

Production Manager, Media Production Company - Samantha Fekete

Sep 23, 2022