How to Test Webhooks: A Practical Guide for Developers
Webhooks are simple in theory. A service sends an HTTP POST to your server when something happens. But actually testing them during development? That's where things get annoying fast.
If you're used to REST APIs where you make a request and get a response, webhooks flip the model. You don't call them. They call you. And that one difference creates a whole set of problems when you're trying to build and debug an integration.
Why testing webhooks is tricky
Three things make webhook testing harder than it needs to be.
Your server needs to be publicly reachable. When Stripe or GitHub sends a webhook, it's coming from their infrastructure to a URL you've registered. Your localhost:8000 isn't accessible from the internet. During development, you need some way to bridge that gap.
Payloads are hard to read. A single Stripe checkout.session.completed event can be 200+ lines of nested JSON. Figuring out which fields you actually care about means scrolling through a wall of data. Multiply that by a dozen event types and it gets tedious.
You can't easily replay them. Webhooks are fire-and-forget. If your handler crashes or you weren't logging the payload, you have to trigger the event all over again. That might mean completing another test purchase, pushing another commit, or waiting for something to happen organically.
The common approaches
Stop reading raw JSON
Payloader shows you what your webhooks actually did — in plain English. See the event, status, and key data at a glance.
Start free trial →There's no single "right" way to test webhooks. It depends on what you're trying to do. Here are the four approaches most developers use.
1. Webhook inspection tools
A webhook testing tool gives you a public URL that captures incoming requests so you can inspect the headers, body, and metadata. You point the provider at this URL, trigger an event, and see exactly what arrives.
Payloader takes this a step further by translating raw JSON payloads into plain-English summaries. Instead of parsing through nested objects to figure out what happened, you get something like "Customer [email protected] completed checkout for $49.00." That's useful when you're figuring out which fields to extract before writing any handler code.
2. Tunnel tools (ngrok, Cloudflare Tunnel)
Tunnels expose your local development server to the internet through a temporary public URL. You run your app locally, point the webhook provider at the tunnel URL, and requests flow straight to your machine. This is great for end-to-end testing where you want to actually execute your handler code.
3. Provider CLI tools
Some providers ship their own CLI for webhook testing. The Stripe CLI and GitHub CLI can both forward webhook events to your local server. These tools are handy because they handle the tunneling for you and can generate test events on demand.
4. Writing your own test endpoint
The simplest approach is just spinning up a minimal server that logs incoming requests. It's quick, it works, and you don't need any third-party tools. The downside is that you're building throwaway infrastructure every time.
Step-by-step: Testing with a webhook inspection tool
This is the fastest way to see what a webhook payload actually looks like before you write any handler code.
- Create a free endpoint on Payloader. You'll get a unique URL like
https://hook.payloader.dev/e/abc123def456. - Go to your webhook provider's settings (Stripe Dashboard, GitHub repo settings, Shopify admin, etc.) and add that URL as a webhook endpoint.
- Trigger an event. For Stripe, make a test purchase. For GitHub, push a commit. For Shopify, update a product.
- Go back to Payloader and inspect the payload. You'll see the raw JSON, headers, and (for supported platforms) a human-readable summary of what the event means.
- Use what you've learned to write your handler. You now know the exact payload structure, which fields contain the data you need, and what edge cases to watch for.
This works well as a first step because you don't need any local setup. You're just observing what the provider sends. Once you understand the payload, move on to local testing with a tunnel or CLI.
Step-by-step: Testing with ngrok
ngrok creates a tunnel from a public URL to your local machine. Here's the setup.
First, make sure your local server is running. For this example, let's say it's a Python server on port 8000 with a webhook endpoint at /webhooks/stripe/.
# Start your local server
python manage.py runserver 8000
In a separate terminal, start ngrok:
# Expose port 8000 to the internet
ngrok http 8000
ngrok will print a forwarding URL like https://a1b2c3d4.ngrok-free.app. Copy that URL.
Now register the full webhook URL with your provider:
# Example: registering via curl with Stripe
curl https://api.stripe.com/v1/webhook_endpoints \
-u sk_test_...: \
-d url="https://a1b2c3d4.ngrok-free.app/webhooks/stripe/" \
-d "enabled_events[]"="checkout.session.completed"
Trigger an event and watch it hit your local server. ngrok also has a local inspector at http://127.0.0.1:4040 where you can see request details and replay them. That replay feature is genuinely useful when your handler is crashing and you need to iterate.
One thing to watch out for: ngrok free-tier URLs change every time you restart the tunnel. That means you need to update your webhook registration each time. Paid plans give you a stable subdomain.
Step-by-step: Testing with Stripe CLI
The Stripe CLI is the most convenient option if you're specifically working with Stripe webhooks. It handles tunneling, event generation, and signature verification all in one tool.
Install the CLI and log in:
# macOS
brew install stripe/stripe-cli/stripe
# Log in to your Stripe account
stripe login
Start listening and forwarding to your local server:
stripe listen --forward-to localhost:8000/webhooks/stripe/
The CLI prints a webhook signing secret (starts with whsec_). Use this secret in your local environment, not your live Dashboard secret. They're different keys.
In another terminal, trigger a test event:
# Trigger a specific event type
stripe trigger checkout.session.completed
# Or trigger a payment intent flow
stripe trigger payment_intent.succeeded
The event will hit your local endpoint with a valid signature. You can see the full payload in the CLI output, and your handler runs locally so you can set breakpoints, check logs, and debug normally.
The Stripe CLI approach is great for Stripe specifically, but it obviously won't help you test webhooks from GitHub, Shopify, or Linear. For those providers, use a tunnel or an inspection tool.
Tips for production webhook debugging
Testing in development is one thing. Debugging webhook issues in production is another. Here are some things that will save you time.
Log the raw payload before processing. If your handler throws an exception, you want the original request body available so you can reproduce the issue locally. Store it somewhere you can access later.
Return 200 quickly, process later. Most webhook providers will retry if they don't get a 2xx response within a few seconds. If your handler does heavy processing synchronously, you risk timeouts and duplicate deliveries. Accept the webhook, queue the work, and return immediately.
# Good: acknowledge fast, process async
@csrf_exempt
def webhook_handler(request):
payload = json.loads(request.body)
process_webhook.delay(payload) # Celery task
return HttpResponse(status=200)
Handle idempotency. Webhooks can (and will) be delivered more than once. Use the event ID to deduplicate. If you've already processed event evt_1234, skip it on the second delivery.
Monitor your endpoint. Set up alerting for webhook failures. If your endpoint starts returning 500s, you want to know before your customers notice that their orders aren't being fulfilled or their accounts aren't being provisioned.
Use a webhook debugger for ongoing visibility. Having a persistent record of every webhook your endpoint receives (with parsed, readable summaries) makes it much easier to trace issues after the fact. It beats digging through application logs.
Picking the right approach
If you just want to see what a webhook payload looks like, use an inspection tool. If you need to test your actual handler code locally, use ngrok or a provider CLI. In production, log everything and make sure you can replay events when things go wrong.
Most developers end up using a combination. Inspect the payload first to understand the structure, then switch to a tunnel or CLI to test the handler, then use a monitoring tool in production to catch issues early.
Whatever approach you pick, the goal is the same: remove the guesswork. Webhooks don't have to be a black box. With the right tooling, you can see exactly what's being sent, verify your handler does the right thing, and debug confidently when something breaks.