How to Debug Stripe Webhooks: A Complete Guide
Stripe webhooks are the backbone of modern payment integrations, but when they break, they can be frustratingly difficult to debug. Whether you're dealing with signature verification failures, missing events, or mysterious 500 errors, this guide will walk you through everything you need to know to debug Stripe webhooks like a pro.
Understanding Stripe Webhooks
Before diving into debugging, let's quickly recap what Stripe webhooks are. When events happen in your Stripe account—like a successful payment, failed charge, or subscription update—Stripe sends an HTTP POST request to your webhook endpoint with event data. Your application needs to:
- Receive the webhook request
- Verify the signature to ensure it's genuinely from Stripe
- Process the event data
- Return a 200 status code within 30 seconds
When any of these steps fail, you'll need to debug the issue. Let's break down the most common problems and their solutions.
Step 1: Verify Webhook Signatures
Stripe signs every webhook with a secret key to prevent malicious actors from sending fake events to your endpoint. The most common webhook failure is signature verification errors. Here's how to properly verify signatures:
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
app.post('/webhook', express.raw({type: 'application/json'}), (request, response) => {
const sig = request.headers['stripe-signature'];
let event;
try {
// Construct and verify the event
event = stripe.webhooks.constructEvent(request.body, sig, endpointSecret);
} catch (err) {
// Invalid signature
console.error(`Webhook signature verification failed: ${err.message}`);
return response.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 was successful!');
break;
default:
console.log(`Unhandled event type ${event.type}`);
}
response.json({received: true});
});
express.raw() middleware for your webhook route, not express.json(). Stripe's signature verification requires the raw request body. If you parse the JSON first, signature verification will always fail.
Step 2: Testing Stripe Webhooks Locally
You can't point Stripe directly at localhost, so you have three options for local testing:
Option 1: Stripe CLI (Recommended)
The Stripe CLI is the official way to test webhooks locally. It creates a secure tunnel between Stripe and your local machine:
# Install Stripe CLI
brew install stripe/stripe-cli/stripe
# Login to your Stripe account
stripe login
# Forward webhooks to localhost
stripe listen --forward-to localhost:3000/webhook
# Trigger test events
stripe trigger payment_intent.succeeded
The CLI will output your webhook signing secret. Add it to your .env file as STRIPE_WEBHOOK_SECRET.
Option 2: Use a Webhook Testing Service
If you need more advanced debugging features like request inspection, automatic retries, or testing multiple endpoints simultaneously, consider using HubHook. You get a public URL instantly, can inspect every request header and body, and relay webhooks to your local development server.
# Create a HubHook endpoint
curl -X POST https://api.hubhook.io/v1/endpoints \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{"name": "Stripe Development", "relay_to": "http://localhost:3000/webhook"}'
# Use the returned URL in Stripe Dashboard
# https://hooks.hubhook.io/ep_abc123
Option 3: ngrok
While ngrok works, it has limitations: URLs change on restart (unless you pay for a static domain), no request inspection UI, and you're manually tunneling rather than using Stripe's official tooling.
Step 3: Debugging Common Stripe Webhook Issues
Problem: "No signatures found matching the expected signature"
Causes:
- Using
express.json()instead ofexpress.raw() - Wrong webhook secret (test vs. production)
- Request body was modified before verification
Solution: Ensure your webhook route uses raw body parsing and the correct secret for your environment.
Problem: Webhooks arrive but aren't processed
Causes:
- Your handler is throwing an error
- Async operations aren't awaited properly
- Event type isn't handled in your switch statement
Solution: Add comprehensive logging and wrap your event handling in try-catch blocks:
try {
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSuccess(event.data.object);
break;
case 'charge.failed':
await handlePaymentFailure(event.data.object);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
} catch (error) {
console.error(`Error processing ${event.type}:`, error);
// Still return 200 to prevent retries for this event
return response.status(200).json({
received: true,
error: error.message
});
}
Problem: Duplicate Events
Stripe may send the same event multiple times if your endpoint doesn't respond quickly enough. Implement idempotency by storing processed event IDs:
const processedEvents = new Set(); // Use Redis in production
if (processedEvents.has(event.id)) {
console.log(`Event ${event.id} already processed`);
return response.json({received: true});
}
// Process the event...
processedEvents.add(event.id);
Step 4: Monitoring and Alerts
Once your webhooks are working, set up monitoring to catch issues before they impact customers:
- Stripe Dashboard: Check the "Developers → Webhooks" page for failed attempts
- Application Logging: Log every webhook event type and processing result
- Monitoring Tools: Tools like HubHook provide real-time alerts when webhooks fail, with detailed error logs and automatic retry capabilities
- Analytics: Track webhook processing latency to ensure you're responding within 30 seconds
Best Practices for Stripe Webhooks
- Respond quickly: Return a 200 status immediately, then process the event asynchronously
- Handle all event types gracefully: Don't crash on unknown events; just log them
- Implement idempotency: Track processed event IDs to prevent duplicate processing
- Use separate endpoints for test and production: Never mix test and live webhook secrets
- Monitor your endpoints: Set up alerts for webhook failures
- Version your webhook handlers: When making breaking changes, create new endpoints rather than modifying existing ones
Debug Stripe Webhooks in Minutes, Not Hours
HubHook gives you instant webhook endpoints, real-time inspection, signature verification, and automatic retries. Stop wrestling with webhook debugging and start shipping faster.
Try HubHook Free →Conclusion
Debugging Stripe webhooks doesn't have to be painful. By implementing proper signature verification, setting up local testing with the Stripe CLI or a webhook debugging tool, handling errors gracefully, and monitoring your endpoints, you can build rock-solid payment integrations that just work.
Remember: most webhook issues come down to signature verification problems or slow response times. Start there when debugging, and you'll solve 90% of issues quickly.
For more webhook debugging guides, check out our posts on webhook security best practices and testing GitHub webhooks locally. And if you're building applications that rely on blockchain analytics or social media monitoring, explore tools like ChainOptics for crypto intelligence and Stack Stats Apps for developer productivity tools.