Complete Guide to Stripe Webhook Setup and Testing
Stripe webhooks are essential for handling asynchronous payment events in your SaaS application. This guide walks you through everything from initial setup to production deployment, including local testing strategies and security best practices.
Why Webhooks Matter
When a customer completes a payment, Stripe needs a way to notify your application. While you could poll the Stripe API, webhooks provide real-time, event-driven notifications that are more efficient and reliable. Webhooks handle critical events like successful payments, failed charges, subscription renewals, and refunds.
Without proper webhook handling, you might miss important events, leading to inconsistent application state, unpaid subscriptions, or customer service issues. This guide ensures you implement webhooks correctly from day one.
Setting Up Your Webhook Endpoint
First, create a webhook endpoint in your Express.js backend. This endpoint will receive POST requests from Stripe whenever events occur. Here's a basic implementation:
// server/routes/webhook.routes.js
import express from 'express';
import stripe from 'stripe';
const router = express.Router();
const stripeClient = stripe(process.env.STRIPE_SECRET_KEY);
// Important: This route must NOT use body-parser middleware
// Stripe needs the raw body to verify the signature
router.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => {
const sig = req.headers['stripe-signature'];
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
let event;
try {
event = stripeClient.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}`);
}
// Handle the event
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object;
// Update your database, send confirmation email, etc.
console.log('Payment succeeded:', paymentIntent.id);
break;
case 'customer.subscription.created':
const subscription = event.data.object;
// Activate user subscription in your database
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
res.json({received: true});
});
export default router;Notice the use of express.raw() middleware. This is crucial because Stripe needs the raw request body to verify the webhook signature. If you parse the body as JSON first, signature verification will fail.
Testing Locally with ngrok
Stripe needs a publicly accessible URL to send webhooks, but your local development server isn't accessible from the internet. ngrok creates a secure tunnel to your localhost, making it accessible to Stripe.
Install ngrok and start a tunnel:
# Install ngrok (macOS) brew install ngrok # Start tunnel to your local server (port 5000) ngrok http 5000 # You'll get a URL like: https://abc123.ngrok.io # Use this as your webhook endpoint in Stripe Dashboard
In the Stripe Dashboard, go to Developers → Webhooks and add your ngrok URL. Stripe will provide a webhook signing secret—add this to your .env file as STRIPE_WEBHOOK_SECRET.
Production Deployment with Cloudflare
For production, you'll use Cloudflare Tunnel (formerly Argo Tunnel) to securely expose your local backend. This is more reliable than ngrok and doesn't require exposing your server's IP address.
Set up Cloudflare Tunnel:
# Install cloudflared
# macOS: brew install cloudflared
# Linux: Download from cloudflare.com
# Authenticate
cloudflared tunnel login
# Create tunnel
cloudflared tunnel create diggabyte-api
# Configure tunnel (config.yml)
tunnel: diggabyte-api
credentials-file: /path/to/credentials.json
ingress:
- hostname: api.diggabyte.com
service: http://localhost:5000
- service: http_status:404
# Run tunnel
cloudflared tunnel run diggabyte-apiUpdate your Stripe webhook endpoint to https://api.diggabyte.com/api/webhook and use the webhook secret from your production Stripe account.
Security Best Practices
- •Always verify webhook signatures: Never process webhook events without verifying the signature. This prevents malicious actors from sending fake events to your endpoint.
- •Use idempotency keys: Stripe events can be delivered multiple times. Use idempotency keys or check if you've already processed an event before taking action.
- •Log all webhook events: Keep detailed logs of received webhooks for debugging and auditing. Store event IDs to detect duplicates.
- •Handle errors gracefully: Return appropriate HTTP status codes. Return 200 for successful processing, 400 for invalid requests, and 500 for processing errors.
Common Webhook Events
Here are the most important webhook events you should handle:
payment_intent.succeeded
Fired when a payment is successfully completed. Update your database to mark the order as paid.
customer.subscription.created
New subscription activated. Grant the user access to premium features.
customer.subscription.deleted
Subscription cancelled. Revoke premium access and notify the user.
charge.refunded
Payment refunded. Update order status and inventory if applicable.
Testing Your Webhook Handler
Stripe provides a webhook testing tool in the Dashboard. You can send test events to your endpoint to verify everything works correctly. Additionally, use the Stripe CLI for local testing:
# Install Stripe CLI # macOS: brew install stripe/stripe-cli/stripe # Linux: See stripe.com/docs/stripe-cli # Login stripe login # Forward webhooks to local server stripe listen --forward-to localhost:5000/api/webhook # Trigger test events stripe trigger payment_intent.succeeded
Conclusion
Proper webhook implementation is critical for reliable payment processing. By following this guide, you'll have a secure, tested webhook handler that can handle production traffic. Remember to always verify signatures, handle idempotency, and log events for debugging.
Our Full-Stack SaaS template includes a complete webhook implementation with all these best practices built-in. Download it to see a production-ready example you can use as a starting point.
Ready to Ship Your SaaS?
Stop writing the same boilerplate code. Browse our production-ready SaaS templates — each includes authentication, Stripe billing, database setup, and deployment configs so you can launch in days instead of months.
Check out our straightforward pricing, see how we compare to ShipFast and MakerKit, or read the documentation to understand exactly what you get. Questions? Learn about our team and mission, or catch up on our engineering blog.