How to Test Stripe Webhooks in 2025 (Complete Guide)

Learn how to test Stripe webhooks locally and in production. Step-by-step guide with code examples, best practices, and troubleshooting tips.

October 14, 2025
12 min read
By Josh, Founder

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:

  1. They're POST requests to your server - Not visible in browser console
  2. They happen asynchronously - May arrive seconds or minutes after the action
  3. They require signature verification - You need to validate the webhook came from Stripe
  4. 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

  1. Login to your Stripe account:
stripe login
  1. Forward webhooks to your local server:
stripe listen --forward-to localhost:3000/api/webhooks/stripe
  1. 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

  1. Click "Create Endpoint" in your dashboard
  2. Name it "Stripe Webhooks - Test" (or anything you prefer)
  3. Add a description: "Testing Stripe subscription webhooks"
  4. Click "Create"

You'll instantly get a unique webhook URL like:

https://webhookdebugger.com/webhooks/abc123xyz

Step 3: Add Webhook URL to Stripe Dashboard

  1. Go to your Stripe Dashboard → Webhooks
  2. Click "Add endpoint"
  3. Paste your WebhookDebugger URL
  4. Select events to listen for (or select "All events" for testing)
  5. 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:

  1. Check that you're using the raw request body (not JSON.parse'd)
  2. Verify STRIPE_WEBHOOK_SECRET matches the secret in Stripe Dashboard
  3. Make sure you're passing the stripe-signature header correctly
  4. 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:

  1. Local development? Use Stripe CLI for quick testing
  2. Need persistence? Use WebhookDebugger for 24/7 webhook URLs
  3. 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