The Complete Guide to Debugging Webhooks in 2026
A practical, end-to-end guide to debugging webhooks — from capturing your first payload to verifying your handler actually committed the side-effect.
Webhooks are the connective tissue of modern SaaS. They're also one of the hardest things to debug.
The payload arrives asynchronously. It may arrive multiple times. You can't replay it from your browser. Your local server isn't publicly accessible. The provider's error messages are often minimal.
This guide covers the complete webhook debugging workflow — from capturing your first payload to proving your handler committed the right side-effect. For a focused checklist of what to check when events go missing, see the webhook debugging checklist. For a deep dive on why a 200 response isn't enough, read why delivered doesn't mean applied.
Step 1: Capture the raw payload
Before you can debug anything, you need to see what's actually being sent.
The mistake most developers make: they write the handler first, then try to match the payload to their code. The right order: capture the raw payload first, understand its shape, then write the handler.
Use a webhook capture URL to receive and inspect the raw event:
- Go to HookTunnel and click "Generate Hook URL"
- Copy the URL (e.g.,
https://hooks.hooktunnel.com/h/abc123xyz) - Paste it as your webhook endpoint in Stripe, Twilio, or your provider's dashboard
- Trigger a test event from your provider
You'll see the exact payload — headers, body, everything — in the HookTunnel dashboard within seconds. No server required.
Step 2: Verify the signature
Stripe, Twilio, and most major providers sign their webhook payloads with an HMAC-SHA256 signature. Always verify it before processing. Skipping signature verification means any attacker who finds your endpoint URL can inject fake payment events. See Stripe webhook documentation for the full spec, and our guide to webhook signature verification for implementation across all major providers.
Stripe:
const sig = req.headers['stripe-signature']
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET
try {
const event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret)
// event is verified
} catch (err) {
console.log(`Signature verification failed: ${err.message}`)
return res.status(400).send('Webhook signature verification failed')
}
Twilio:
const twilioSig = req.headers['x-twilio-signature']
const url = 'https://your-domain.com/webhooks/twilio'
const params = req.body // For POST requests with form-encoded body
const isValid = twilio.validateRequest(
process.env.TWILIO_AUTH_TOKEN,
twilioSig,
url,
params
)
Common signature verification failures:
- Wrong webhook secret — make sure you're using the signing secret from the correct environment (test vs live)
- Body already parsed — signature verification requires the raw body, not parsed JSON. Use
express.raw()before the route, notexpress.json() - URL mismatch — Twilio includes the full URL in signature calculation; any mismatch fails
Step 3: Test locally without exposing your server
Once you know the payload shape, you need to develop your handler against real events. But your local server isn't accessible from the internet.
Options:
Option A: Local tunnel HookTunnel can forward captured events to your local server via WebSocket. The webhook URL in your provider dashboard stays the same — no reconfiguring every time your tunnel URL changes.
hooktunnel forward --hook abc123xyz --to http://localhost:3000/webhooks/stripe
Events that arrived while your tunnel was offline are stored. You can replay them when you reconnect.
Option B: Replay from captured history Trigger events in your provider's dashboard, let them capture in HookTunnel, then replay them against localhost as you develop:
- Send test events from Stripe/Twilio dashboard
- They capture in HookTunnel (your handler returns 200 or errors — doesn't matter yet)
- Run your local server
- Replay the captured events
Option C: Test event from dashboard For common provider event types, use HookTunnel's "Send Test Event" — it fires a schema-accurate synthetic payload without touching the real provider.
Step 4: Inspect delivery attempts
When an event isn't processing correctly, look at the full delivery history:
- Status code returned by your handler
- Response body (useful if your handler returns error details)
- Latency (timeout issues show up here)
- Attempt count (how many times the provider has retried)
- Headers (content-type, signature headers)
For Stripe specifically: the provider's event log and HookTunnel's capture log should match. If an event appears in Stripe's log but not in HookTunnel's, your webhook URL may be misconfigured.
Step 5: Check whether your handler actually ran
This is the step most debugging guides skip.
A 200 status code from your handler doesn't mean your handler ran completely. It means the HTTP request was successful. Understanding the gap between delivery and application is critical — read why delivered doesn't mean applied for the full explanation, and webhook revenue leakage for the business cost of silent processing failures.
Your handler can return 200 and then:
- Fail to write to the database
- Throw an exception in async code after the response was sent
- Drop a message queue item
- Commit a partial transaction that rolled back
The only way to know your handler ran completely is to check the output: did the database row get created? Did the subscription get updated?
In HookTunnel, you can set up outcome receipts — a POST call your handler makes after successfully committing the side-effect. The dashboard then shows "Applied Confirmed" for that event, giving you cryptographic proof that the full processing chain ran.
Step 6: Common failure patterns and fixes
Pattern: handler times out
Symptoms: Provider shows retry attempts; your logs show the request arriving but no response logged.
Cause: Your handler is doing synchronous work that takes too long. Stripe times out after 30 seconds.
Fix: Return 200 immediately, process asynchronously:
app.post('/webhooks/stripe', async (req, res) => {
const event = stripe.webhooks.constructEvent(...)
// Return 200 immediately
res.sendStatus(200)
// Process asynchronously
await processWebhookEvent(event).catch(err => {
logger.error({ err, eventId: event.id }, 'Webhook processing failed')
})
})
Pattern: duplicate processing on retry
See our guide on Stripe duplicate webhook events. Webhook retry storms — where providers hammer a recovering endpoint — are covered in webhook retry storms.
Pattern: events arriving out of order
Webhooks don't guarantee order. customer.updated may arrive before customer.created. Build your handler to be order-independent, or use the event timestamp to detect and handle out-of-order delivery.
Pattern: test events work, production events fail
The most common cause: different secrets in test vs. live mode. Verify you're using the live webhook signing secret for production events.
Second common cause: your handler checks for specific event types that behave slightly differently in live mode. Capture a real live event (use HookTunnel with a test purchase) and compare to your test event.
Step 7: Verify outcomes over time
Good webhook debugging isn't just about fixing the immediate failure — it's about knowing your system is healthy over time. The Applied Confirmed rate is the only metric that proves your webhooks are actually working end-to-end. Refer to the HookTunnel pricing page for Pro plan details, which includes 30-day history and outcome receipts for continuous verification. See also the HTTP reference for understanding status codes your monitoring should track.
Key metrics to watch:
- Applied Confirmed rate: what % of delivered events are confirmed by receipts?
- SLA breach rate: how often does receipt confirmation take longer than expected?
- Reconciliation gaps: any paid events without confirmed application in the last 24h?
These metrics tell you whether your webhook processing is reliable — not just whether it's working right now.
HookTunnel gives you every tool in this guide in one place: capture, inspect, replay, and outcome verification. Explore webhook inspection features or start for free → — no signup required.
Stop guessing. Start proving.
Generate a webhook URL in one click. No signup required.
Get started free →