· 10 min read

How to Test Webhooks Locally: The Complete Guide

By HookCap Team

How to Test Webhooks Locally: The Complete Guide

Every webhook integration hits the same wall during development: the service sending webhooks needs a public HTTPS URL, but your code is running on localhost:3000. Stripe, GitHub, Shopify, Slack — none of them can reach your laptop.

This guide covers every practical approach to solving this, from quick tunnel setups to persistent capture-and-forward workflows. By the end, you will know which approach fits your situation and how to set it up.

The Core Problem

Webhooks are HTTP callbacks. When an event occurs (a payment succeeds, a pull request is opened, an order is placed), the service sends an HTTP POST request to a URL you’ve configured. That URL must be:

  1. Publicly reachable — the service’s servers need to connect to it over the internet
  2. HTTPS — most webhook providers require TLS
  3. Responsive — most providers expect a 2xx response within 3-30 seconds

Your local development server meets none of these requirements. It’s behind NAT, doesn’t have a TLS certificate, and isn’t reachable from outside your network.

Approach 1: Tunnel Tools (ngrok, Cloudflare Tunnel, localtunnel)

The most common approach: run a tunnel that creates a public URL pointing to your local server.

ngrok

# Install
brew install ngrok

# Start tunnel to your local server
ngrok http 3000

ngrok gives you a URL like https://a1b2c3d4.ngrok-free.app that forwards traffic to localhost:3000. Register this URL with your webhook provider, and events flow through ngrok to your local server.

Pros:

  • Fast setup — running in under a minute
  • Supports HTTPS out of the box
  • Built-in request inspector at http://127.0.0.1:4040

Cons:

  • URL changes every time you restart (free tier)
  • No persistent event history — if your server is down when the webhook fires, the event is lost
  • Your local server must be running to receive events
  • Free tier has rate limits and connection limits
  • Adds latency to every request (your traffic routes through ngrok’s servers)

Cloudflare Tunnel (cloudflared)

# Install
brew install cloudflared

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

Pros:

  • Free, no account required for quick tunnels
  • Cloudflare’s network is fast
  • Can be configured with a permanent subdomain (requires Cloudflare account)

Cons:

  • Same fundamental limitations as ngrok: no persistence, server must be running, URL may change
  • Quick tunnel URLs are temporary

localtunnel

# Install
npm install -g localtunnel

# Start tunnel
lt --port 3000

Pros:

  • Open source, free
  • Simple setup

Cons:

  • Less reliable than ngrok or Cloudflare
  • Same limitations: no persistence, no event history

When tunnels work well

Tunnels are the right choice when:

  • You need to get something working in the next 5 minutes
  • You are doing a one-off test, not ongoing development
  • Your local server is already running and you just need to expose it

Tunnels become painful when:

  • You restart your tunnel and have to update the webhook URL in every service
  • Your laptop goes to sleep and you miss webhook deliveries
  • You need to replay a specific event and have to re-trigger it in the source service
  • Multiple team members need to test the same webhook integration

Approach 2: Mock Webhook Servers

Instead of receiving real webhooks, simulate them locally with canned payloads.

Manual curl

The simplest version — send a POST request to your local server with a webhook payload:

curl -X POST http://localhost:3000/webhooks/stripe \
  -H "Content-Type: application/json" \
  -H "Stripe-Signature: t=1234567890,v1=abc123..." \
  -d '{
    "id": "evt_test_123",
    "type": "checkout.session.completed",
    "data": {
      "object": {
        "id": "cs_test_456",
        "amount_total": 2000,
        "currency": "usd",
        "customer": "cus_test_789",
        "payment_status": "paid"
      }
    }
  }'

Pros:

  • No dependencies, works anywhere
  • Full control over payload content
  • Useful for edge case testing

Cons:

  • You have to construct the payload yourself — and get it wrong
  • Signature verification fails unless you compute a valid signature
  • Real webhook payloads have dozens of fields; hand-crafting them is error-prone
  • Doesn’t test the full delivery chain (network, headers, timing)

Provider CLI tools

Some providers include webhook testing in their CLI:

Stripe CLI:

stripe listen --forward-to localhost:3000/webhooks/stripe
stripe trigger checkout.session.completed

Pros:

  • Generates realistic payloads
  • Stripe CLI computes valid signatures
  • Direct integration with the provider’s event system

Cons:

  • Only works for that specific provider — you need a different tool for each integration
  • Provider CLIs have their own setup, authentication, and version management
  • Some providers don’t have CLI tools at all (GitHub, Shopify, Slack, most SaaS platforms)
  • Event variety is limited to what the CLI supports triggering

When mocks work well

Mocks are good for unit testing individual handler functions, where you control the input and verify the output. They are not a substitute for testing with real webhook deliveries, because:

  • Real payloads have fields and structures you wouldn’t think to include in a mock
  • Timing and ordering of real events differ from what you’d simulate
  • Signature verification, retry behavior, and idempotency only matter with real deliveries

Approach 3: Capture and Forward (HookCap)

A different model: instead of exposing your local server directly or simulating events, use a persistent capture endpoint that records every webhook delivery and optionally forwards it to your local server.

How it works

  1. Create a HookCap endpoint — you get a permanent HTTPS URL like https://hook.hookcap.dev/ep_a1b2c3d4e5f6
  2. Register that URL with your webhook provider (Stripe, GitHub, Shopify, Slack, etc.)
  3. Events are captured — every delivery is stored with full headers, body, and metadata
  4. Auto-forward to localhost — events are simultaneously forwarded to your local dev server (Pro plan)
  5. Replay any event — click any captured event to replay it to any URL, any time
Stripe/GitHub/Slack


   HookCap endpoint (https://hook.hookcap.dev/ep_...)

        ├── Captures & stores full request

        └── Auto-forwards to localhost:3000/webhooks

Setting up HookCap for local development

Step 1: Sign in at hookcap.dev and create an endpoint.

Step 2: Register the endpoint URL with your webhook provider.

Step 3: Configure auto-forward (Pro plan) to point to your local server:

Forward URL: http://localhost:3000/webhooks/stripe

Step 4: Trigger an event (make a test payment, push a commit, post a Slack message).

Step 5: The event appears in your HookCap dashboard and is forwarded to your local server simultaneously.

Why this approach is different

Your URL never changes. Register it once with each webhook provider. It works whether your laptop is on or off, whether your local server is running or not.

Events are never lost. If your local server is down when a webhook fires, the event is still captured. When your server comes back up, replay the missed events.

Replay without re-triggering. Instead of creating another test order in Stripe or pushing another commit to GitHub, replay the exact event you already captured. Same headers, same body, same signature.

Works with every provider. One tool covers Stripe, GitHub, Shopify, Slack, and any other service that sends webhooks. No provider-specific CLIs needed.

Team-friendly. Multiple developers can inspect the same event log without each running their own tunnel.

Comparing the Approaches

Tunnel (ngrok)Mock/CLIHookCap Auto-Forward
Setup time~1 minuteVaries by provider~2 minutes
Persistent URLPaid tier onlyN/AAlways
Event historyNoNoYes
Works when server is offNoN/ACaptures events; replay later
Replay eventsNoManual re-triggerOne click
Signature verificationWorksRequires manual setupWorks (original signatures preserved)
Multi-providerSeparate tunnel per portSeparate CLI per providerSingle endpoint or multiple
CostFree tier limited; paid $8+/moFree (provider CLIs)Free tier; Pro $12/mo

Which approach to use when

Use a tunnel when you need to expose a local server for a quick demo, a one-off integration test, or a provider that requires real-time response verification (like Slack URL verification). Tunnels are the fastest to start, but the least sustainable for ongoing development.

Use mocks in your automated test suite, where you need deterministic inputs and fast execution. Don’t rely on mocks as your only testing — they diverge from reality.

Use HookCap as your primary webhook development workflow. Capture real events, inspect payloads, forward to localhost for live development, and replay for iteration. It covers the full development cycle without the fragility of tunnels or the limitations of mocks.

Practical Workflows

Workflow 1: Building a new webhook handler

You are adding Stripe webhook handling to your app for the first time.

  1. Create a HookCap endpoint and register it in Stripe’s webhook settings
  2. Make a test payment in Stripe’s test mode
  3. Inspect the captured checkout.session.completed event in HookCap — see the exact payload structure, headers, and Stripe-Signature format
  4. Write your handler based on the real payload, not the docs (docs sometimes lag behind the actual API)
  5. Enable auto-forward to localhost:3000/webhooks/stripe
  6. Replay the captured event to test your handler
  7. Iterate: modify handler, replay, check response — no need to make another test payment

Workflow 2: Debugging a failing webhook handler

Your Shopify order processing broke in production. You have the error log but need to reproduce the exact payload.

  1. Find the failing event in HookCap’s event history (it was captured when it originally fired)
  2. Replay it to your local server with the debugger attached
  3. Step through the handler with the exact payload that caused the failure
  4. Fix the bug, replay again to verify
  5. Deploy with confidence — you tested against the real payload, not a mock

Workflow 3: Testing idempotency

Your webhook handler should be idempotent — processing the same event twice should not create duplicate records.

  1. Capture a payment_intent.succeeded event from Stripe
  2. Replay it to your local server — first run creates the record
  3. Replay it again — second run should detect the duplicate and skip processing
  4. Check your database: only one record exists

This takes seconds with replay. Without it, you would need to create two separate test payments or manually curl the same payload twice.

Workflow 4: Multi-provider integration testing

Your app receives webhooks from Stripe (payments), GitHub (deployments), and Slack (notifications).

  1. Create three HookCap endpoints, one per provider
  2. Register each endpoint in the respective provider settings
  3. All events are captured in one dashboard
  4. Auto-forward all three to different local routes: /webhooks/stripe, /webhooks/github, /webhooks/slack
  5. Test the full flow: a GitHub push triggers a deployment, which triggers a Stripe charge, which sends a Slack notification

Signature Verification During Local Testing

Most webhook providers sign their payloads (Stripe, Shopify, Slack, GitHub). During local development, you need signatures to validate correctly — or you need to know when to skip validation.

Signatures work with replay

When you replay an event from HookCap, the original headers are preserved — including the signature. Since the payload body hasn’t changed, the signature is still valid against the same signing secret.

Requirement: your local environment must have the same webhook secret as the environment where the event was originally captured.

Timestamp staleness checks

Some providers include a timestamp in the signature and reject events older than a threshold (Stripe: 5 minutes, Slack: 5 minutes). When replaying old events, the timestamp check fails.

For development, conditionally skip the staleness check:

// Stripe example
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;

if (process.env.NODE_ENV === 'development') {
  // Skip timestamp tolerance for replayed events
  event = stripe.webhooks.constructEvent(
    body, signature, endpointSecret, 432000 // 5 day tolerance
  );
} else {
  event = stripe.webhooks.constructEvent(body, signature, endpointSecret);
}

Common Mistakes When Testing Webhooks Locally

Not preserving the raw request body

Signature verification requires the raw bytes of the request body. If your web framework parses JSON before your verification middleware runs, the signature check fails. Always mount webhook routes with raw body parsing:

// Express
app.post('/webhooks/stripe',
  express.raw({ type: 'application/json' }),
  stripeWebhookHandler
);

// Mount before global JSON parser
app.use(express.json());

Assuming webhooks arrive in order

Webhook providers do not guarantee delivery order. A payment_intent.succeeded event might arrive before checkout.session.completed if the payment service processes faster than the checkout service. Build your handlers to be order-independent.

Not handling retries

If your endpoint returns a non-2xx status, most providers retry — sometimes for hours or days. During development, a bug in your handler can cause a backlog of retries. Fix the bug, then process the retry queue. Don’t suppress retries during development, because retry handling is part of what you need to test.

Testing only the happy path

Webhook handlers need to handle partial payloads, missing fields, and unexpected event types. Use HookCap’s event history to find edge cases: events with null fields, events with unexpected structures, events from API version changes. Replay these edge cases against your handler.

Getting Started

  1. Sign up at hookcap.dev — free tier available
  2. Create an endpoint
  3. Register it with your webhook provider
  4. Trigger an event and inspect it in HookCap
  5. Use one-click replay to test your handler (available on all plans)
  6. Upgrade to Pro ($12/month) for auto-forward to localhost and longer retention

Your webhook URL is permanent. Register it once, and it works for the entire development lifecycle of your integration — no more updating URLs when your tunnel restarts.


Free tier available at hookcap.dev — includes capture, inspect, replay, and real-time streaming. Pro plans start at $12/month for auto-forward to localhost and longer history retention.