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.
Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
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:
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
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:
# 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
# Trigger a specific event
stripe trigger payment\_intent.succeeded
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:
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:
// Never skip this step
event = stripe.webhooks.constructEvent(req.rawBody, sig, endpointSecret);
// Load from environment variables
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
Use HTTPS for your webhook endpoint
Implement proper access controls for your webhook endpoint
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:
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.