How to Test Stripe Webhooks in 2025 (Complete Guide)
Testing Stripe webhooks can feel frustrating. You need a public URL, signature verification is confusing, and debugging production issues is stressful. If you've ever spent hours trying to figure out why your webhook handler isn't receiving events, you're not alone.
Webhooks are critical for any payment system—they notify your application when payments succeed, subscriptions renew, or refunds occur. Without proper webhook handling, your users might pay but never get access to your product. That's a nightmare scenario.
This guide covers everything you need to know about testing Stripe webhooks: from local development with the Stripe CLI to production testing with hosted webhook debuggers. By the end, you'll have a reliable workflow for testing webhooks at any stage of development.
What Are Stripe Webhooks?
Stripe webhooks are HTTP POST requests that Stripe sends to your server when events occur in your Stripe account. Instead of constantly polling Stripe's API to check for updates, webhooks push data to you in real-time.
Common Stripe Webhook Events
Here are the most frequently used Stripe webhook events:
- payment_intent.succeeded - A payment was successful
- customer.subscription.created - A new subscription started
- customer.subscription.updated - A subscription changed (upgrade/downgrade)
- customer.subscription.deleted - A subscription was canceled
- invoice.payment_succeeded - Subscription payment went through
- invoice.payment_failed - Payment failed (card declined, etc.)
- charge.refunded - A charge was refunded
- customer.created - A new customer was added
Why You Can't Just Use console.log()
Webhooks happen server-side, often triggered by external actions (like a customer clicking "Subscribe" in Stripe Checkout). You can't debug them with browser dev tools because:
- They're POST requests to your server - Not visible in browser console
- They happen asynchronously - May arrive seconds or minutes after the action
- They require signature verification - You need to validate the webhook came from Stripe
- They need a public URL - Stripe can't send webhooks to
localhost:3000
That's why testing webhooks requires special tools and techniques.
Method 1: Using Stripe CLI (Best for Local Development)
The Stripe CLI is the official tool for testing webhooks during development. It creates a tunnel that forwards webhook events from Stripe to your local server.
Installation
macOS (Homebrew):
brew install stripe/stripe-cli/stripe
Windows (Scoop):
scoop bucket add stripe https://github.com/stripe/scoop-stripe-cli.git
scoop install stripe
Linux:
wget https://github.com/stripe/stripe-cli/releases/latest/download/stripe_linux_x86_64.tar.gz
tar -xvf stripe_linux_x86_64.tar.gz
sudo mv stripe /usr/local/bin
Using the Stripe CLI
- Login to your Stripe account:
stripe login
- Forward webhooks to your local server:
stripe listen --forward-to localhost:3000/api/webhooks/stripe
- Trigger test events:
stripe trigger payment_intent.succeeded
Pros and Cons
Pros:
- Official Stripe tool (most reliable)
- Easy to trigger specific events
- Works perfectly for local development
- Free and open source
Cons:
- Requires installation and setup
- Terminal must stay open
- Can't share webhook URLs with teammates
- Not suitable for testing deployed apps
- Webhooks disappear when you close the terminal
Best for: Local development and quick testing of webhook handlers
Method 2: Using WebhookDebugger (Best for Persistent Testing)
When you need a webhook URL that stays online 24/7, persists history, and works with your deployed application, a hosted webhook debugger is the way to go.
Why You Need a Hosted Solution
The Stripe CLI is great for local testing, but it has limitations:
- Your local server needs to be running
- You can't test webhooks on your staging/production environment
- You can't share webhook data with your team
- History is lost when you close the terminal
- Can't test from mobile devices or different locations
A hosted webhook debugger solves all of these problems.
Step-by-Step Tutorial: Testing Stripe Webhooks with WebhookDebugger
Here's how to test Stripe webhooks using a persistent, hosted endpoint:
Step 1: Create a Free Account
Visit WebhookDebugger and sign up for a free account. No credit card required.
Step 2: Create a New Endpoint
- Click "Create Endpoint" in your dashboard
- Name it "Stripe Webhooks - Test" (or anything you prefer)
- Add a description: "Testing Stripe subscription webhooks"
- Click "Create"
You'll instantly get a unique webhook URL like:
https://webhookdebugger.com/webhooks/abc123xyz
Step 3: Add Webhook URL to Stripe Dashboard
- Go to your Stripe Dashboard → Webhooks
- Click "Add endpoint"
- Paste your WebhookDebugger URL
- Select events to listen for (or select "All events" for testing)
- Click "Add endpoint"
Step 4: Trigger a Test Webhook
In the Stripe Dashboard, click on your newly created webhook endpoint and click "Send test webhook." Choose an event like payment_intent.succeeded
.
Step 5: Inspect the Data in Real-Time
Head back to WebhookDebugger. You'll see the webhook appear instantly with:
- Full request headers
- Complete JSON payload
- Timestamp
- HTTP method (POST)
- IP address
- User agent
You can expand the JSON to inspect nested objects, copy values, and even share the URL with teammates.
Step 6: Test Your Actual Webhook Handler
Once you've confirmed webhooks are arriving, update your production code to handle them:
// Example: Next.js API Route
import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export async function POST(request: NextRequest) {
const body = await request.text()
const signature = request.headers.get('stripe-signature')!
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
)
} catch (err) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
}
// Handle the event
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object as Stripe.PaymentIntent
console.log('Payment succeeded:', paymentIntent.id)
// Update your database, send confirmation email, etc.
break
case 'customer.subscription.created':
const subscription = event.data.object as Stripe.Subscription
console.log('New subscription:', subscription.id)
// Activate user's account
break
default:
console.log(`Unhandled event type: ${event.type}`)
}
return NextResponse.json({ received: true })
}
Advantages of WebhookDebugger
Pros:
- Persistent webhook URLs (stay online 24/7)
- View webhook history for up to 30 days
- Real-time updates (see webhooks as they arrive)
- No installation or CLI required
- Works from any device
- Share webhook URLs with teammates
- Beautiful, searchable interface
- Perfect for testing production deployments
Cons:
- Requires an account signup
- Free plan has limits (upgrade for more endpoints)
Best for: Testing webhooks on deployed applications, sharing data with team members, debugging production issues
Pricing:
- Free: 1 endpoint, 1,000 webhooks/month
- Starter ($9/mo): 10 endpoints, 10,000 webhooks/month
- Pro ($29/mo): 50 endpoints, 100,000 webhooks/month
Method 3: Using ngrok (Mentioned for Completeness)
Some developers use ngrok to expose their local server to the internet. While ngrok is powerful, it's not ideal for webhook testing:
How it works:
ngrok http 3000
# Gives you: https://abc123.ngrok.io
Why it's painful for webhooks:
- URL changes every time you restart ngrok
- You have to update Stripe webhook URL constantly
- Free tier has connection limits
- Requires keeping terminal open
- Not designed specifically for webhooks
Verdict: Use ngrok for exposing local APIs to clients, but use Stripe CLI or WebhookDebugger for webhook testing.
Best Practices for Testing Stripe Webhooks
1. Always Verify Webhook Signatures
Never trust webhook data without verification. Stripe signs every webhook with a secret, and you must verify it:
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export async function POST(request: Request) {
const body = await request.text()
const signature = request.headers.get('stripe-signature')!
try {
const event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
)
// Process the event
return new Response('Success', { status: 200 })
} catch (err) {
console.error('Webhook signature verification failed:', err.message)
return new Response('Invalid signature', { status: 400 })
}
}
The STRIPE_WEBHOOK_SECRET
is found in your Stripe Dashboard under the webhook endpoint settings (starts with whsec_
).
2. Handle Idempotency
Stripe may send the same webhook multiple times (network issues, retries, etc.). Use the event.id
to prevent duplicate processing:
const processedEvents = new Set() // In production, use a database
if (processedEvents.has(event.id)) {
return new Response('Already processed', { status: 200 })
}
processedEvents.add(event.id)
// Process the event...
3. Return 200 Quickly
Stripe expects a response within 5 seconds. If your webhook handler times out, Stripe will retry it (potentially causing duplicate processing).
Bad:
// DON'T do slow operations before responding
await sendEmail()
await updateDatabase()
await notifySlack()
return new Response('OK', { status: 200 })
Good:
// Respond immediately, process async
queueJob(event) // Add to job queue
return new Response('OK', { status: 200 })
4. Log Everything
During testing, log all webhook events to help with debugging:
console.log('Received webhook:', {
id: event.id,
type: event.type,
created: new Date(event.created * 1000),
})
In production, use a proper logging service (Sentry, LogRocket, etc.).
Common Issues & Solutions
Issue: "Webhook timed out"
Why it happens: Your webhook handler is taking too long to respond (>5 seconds).
How to fix:
- Return 200 immediately
- Process webhook data asynchronously (use a job queue)
- Don't make slow API calls before responding
- Optimize database queries
Issue: "Signature verification failed"
Debug steps:
- Check that you're using the raw request body (not JSON.parse'd)
- Verify
STRIPE_WEBHOOK_SECRET
matches the secret in Stripe Dashboard - Make sure you're passing the
stripe-signature
header correctly - Check for any middleware that modifies the request body
Common mistake:
// WRONG - body is already parsed
const body = await request.json()
// RIGHT - get raw text
const body = await request.text()
Issue: "Webhook not arriving"
Checklist:
- Is your server running and publicly accessible?
- Did you add the correct URL in Stripe Dashboard?
- Is your firewall blocking incoming requests?
- Did you select the correct events in Stripe?
- Check Stripe Dashboard → Webhooks → Your endpoint → "Attempts" tab
Testing Locally vs Production
Local testing:
- Use Stripe CLI:
stripe listen --forward-to localhost:3000/webhook
- Use test mode API keys
- Trigger events manually:
stripe trigger payment_intent.succeeded
Production testing:
- Use WebhookDebugger for a persistent webhook URL
- Test with real Stripe Checkout flow (use test cards)
- Use live mode API keys (in production environment only)
- Monitor webhook delivery in Stripe Dashboard
Conclusion
Testing Stripe webhooks doesn't have to be complicated. Here's the recap:
- Local development? Use Stripe CLI for quick testing
- Need persistence? Use WebhookDebugger for 24/7 webhook URLs
- Production debugging? Use WebhookDebugger to inspect live webhook data
The most important things to remember:
- Always verify webhook signatures
- Handle events idempotently
- Return 200 quickly and process async
- Log everything for debugging
Ready to start testing webhooks? Try WebhookDebugger free - no credit card required. Get your webhook endpoint in seconds and start debugging Stripe webhooks like a pro.
Frequently Asked Questions
How do I test Stripe webhooks without deploying?
Use the Stripe CLI to forward webhooks to your local server:
stripe listen --forward-to localhost:3000/api/webhooks/stripe
This creates a tunnel that lets Stripe send webhooks to your local development environment.
What's the best test card for Stripe?
Use these test card numbers in Stripe's test mode:
- Success:
4242 4242 4242 4242
- Decline:
4000 0000 0000 0002
- Requires authentication:
4000 0025 0000 3155
Use any future expiry date, any 3-digit CVC, and any ZIP code.
How long does Stripe retry failed webhooks?
Stripe retries failed webhooks (non-200 responses) for up to 3 days using exponential backoff. The retry schedule:
- Immediately
- After 5 minutes
- After 30 minutes
- After 2 hours
- After 5 hours
- Then every 12 hours for up to 3 days
After 3 days of failures, Stripe stops sending that webhook event.
Last updated: January 2025 | Written by the WebhookDebugger Team