How to Test Webhooks Locally: A Complete Guide

Learn how to test and debug webhooks in your local development environment. Step-by-step guide covering Mitte, ngrok, and other tools for webhook testing.

Testing webhooks locally is one of the most common challenges in API development. Webhook providers like Stripe, GitHub, and Shopify need a publicly accessible URL to send events to — but your localhost:3000 isn't reachable from the internet.

This guide covers the best approaches to solve this problem.

The Challenge

When you register a webhook URL with a service like Stripe, here's what happens:

  1. An event occurs (e.g., a payment succeeds)
  2. Stripe sends an HTTP POST to your registered URL
  3. Your server processes the event

The problem: during development, your server runs on localhost. Stripe can't reach localhost:3000/webhooks/stripe from their servers.

A webhook gateway like Mitte gives you a persistent public URL that receives webhooks and forwards them to your local server — or any destination.

Step-by-Step with Mitte

1. Create an endpoint

Sign up at mitte.run and create a new endpoint. You'll get a URL like:

https://hook.mitte.run/e/your-endpoint-id

2. Set your forwarding destination

Configure the endpoint to forward to your local development server:

http://localhost:3000/api/webhooks/stripe

For local forwarding during development, you'll need a tunneling tool or run Mitte's webhook debugger tool.

3. Register the URL with your webhook provider

In Stripe's dashboard (or any webhook provider), set the webhook URL to your Mitte endpoint URL.

4. Monitor in real-time

Open Mitte's dashboard to see webhook events streaming in real-time. Every request shows:

  • Full headers and body
  • Response status and timing
  • Error details if delivery failed
  • AI-powered error explanations (Pro)

Why This Approach Is Best

  • Persistent URL — No new URL every time you restart your dev server
  • Full history — All events are logged even if your local server is down
  • Auto-retries — Failed deliveries are automatically retried
  • Team-friendly — Share endpoints and logs with teammates
  • AI debugging — Get instant explanations when things break

Method 2: Use a Tunneling Tool

Tunneling tools create a secure tunnel from a public URL to your local machine.

Using ngrok

# Install ngrok
brew install ngrok  # or download from ngrok.com

# Start a tunnel to your local server
ngrok http 3000

ngrok will give you a URL like https://abc123.ngrok-free.app that forwards to localhost:3000.

Pros:

  • Quick setup
  • Traffic inspection UI
  • Request replay

Cons:

  • URL changes on every restart (free plan)
  • No auto-retries
  • No payload transforms
  • Not suitable for production

Using Cloudflare Tunnel

# Install cloudflared
brew install cloudflared

# Quick tunnel (no account needed)
cloudflared tunnel --url http://localhost:3000

Pros:

  • Free
  • DDoS protection
  • Stable URLs with named tunnels

Cons:

  • More complex setup for persistent tunnels
  • No webhook-specific features

Method 3: Use Webhook Provider CLI Tools

Many webhook providers offer CLI tools that forward events to localhost:

Stripe CLI

# Install Stripe CLI
brew install stripe/stripe-cli/stripe

# Login
stripe login

# Forward events to your local server
stripe listen --forward-to localhost:3000/api/webhooks/stripe

GitHub CLI (with smee.io)

# Install smee client
npm install -g smee-client

# Create a channel at smee.io, then:
smee -u https://smee.io/your-channel -t http://localhost:3000/api/webhooks/github

Pros:

  • Official tooling, well-documented
  • Tight integration with the specific provider

Cons:

  • Provider-specific — need different tools for each service
  • Only for development
  • Limited to that provider's events

Method 4: Use Mitte's Webhook Debugger

For quick testing without any setup, use Mitte's free Webhook Debugger:

  1. Visit the tool page — you get an instant URL
  2. Send test webhooks to that URL (via curl, Postman, or your webhook provider)
  3. See requests appear in real-time in your browser
  4. Inspect headers, body, query params, and timing

This is the fastest way to verify that a webhook provider is sending the right payload before writing any handling code.

Best Practices for Webhook Testing

1. Always Verify Signatures

import crypto from 'crypto'

function verifyWebhookSignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex')
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  )
}

2. Return 200 Quickly, Process Async

app.post('/webhooks/stripe', async (req, res) => {
  // Acknowledge receipt immediately
  res.status(200).json({ received: true })
  
  // Process the event asynchronously
  processWebhookEvent(req.body).catch(console.error)
})

3. Handle Idempotency

Webhooks can be delivered more than once. Use the event ID to deduplicate:

const processedEvents = new Set<string>()

async function processWebhookEvent(event: WebhookEvent) {
  if (processedEvents.has(event.id)) {
    return // Already processed
  }
  
  processedEvents.add(event.id)
  // ... handle the event
}

4. Test Error Scenarios

Don't just test the happy path. Simulate:

  • Timeouts — What if your server takes too long to respond?
  • Invalid signatures — Does your verification code reject bad signatures?
  • Duplicate deliveries — Does your idempotency logic work?
  • Malformed payloads — What if the body isn't valid JSON?

Summary

MethodSetup TimeBest ForProduction-Ready
Mitte (gateway)2 minFull webhook workflow
ngrok (tunnel)1 minQuick local testing
Provider CLI5 minSingle-provider testing
Webhook Debugger0 minQuick payload inspection

For most teams, using a webhook gateway during development and in production gives you the best developer experience — persistent URLs, full logging, auto-retries, and no URL changes every time you restart.


Ready to simplify your webhook development? Get started with Mitte →