Webhook Debugging Guide — A Systematic Approach
Systematic approach to debugging webhook issues — delivery logs, signatures, payloads, curl testing, and common HTTP error codes.
HookDeploy Team · May 22, 2026
Webhook bugs are frustrating because the failure is often on someone else’s server sending to yours. This guide gives you a repeatable process to find the problem fast.
Step 1: Confirm the webhook was sent
Before debugging your handler, verify the sender actually dispatched the event.
Provider delivery logs:
- Stripe: Dashboard → Developers → Webhooks → select endpoint → Recent deliveries
- GitHub: Repo Settings → Webhooks → Recent Deliveries
- Shopify: Settings → Notifications → Webhooks → show notifications
Look for the delivery attempt. If there is no entry, the event was not triggered — go back to the provider and fire a test event.
HookDeploy capture: If the sender points at HookDeploy, open your endpoint in the dashboard. If the request is not in the stream, the sender never reached HookDeploy.
Step 2: Check the HTTP response code
The delivery log shows what response the sender received:
| Code | Meaning | Action |
|---|---|---|
| 200 | Success | Sender is happy. Problem is in your handler logic, not delivery |
| 400 | Bad request | Your handler rejected the payload — check signature verification |
| 401 | Unauthorized | Signature verification failed |
| 404 | Not found | Wrong URL — check slug/path |
| 413 | Payload too large | Exceeds HookDeploy plan limit or your server’s limit |
| 422 | Locked | HookDeploy endpoint is paused |
| 429 | Rate limited | Plan limit reached (HookDeploy) or your rate limiter fired |
| 500 | Server error | Your handler crashed — check application logs |
| 502/503 | Gateway error | Your server or tunnel is down |
| Timeout | No response in time | Server too slow or tunnel disconnected |
Step 3: Inspect the payload
Open the request in HookDeploy (or your provider’s delivery log). Check:
- Event type — is it the event you expected? (
payment_intent.succeededvspayment_intent.created) - Payload structure — does
data.objectcontain the fields your handler reads? - Headers — is
Content-Typecorrect? Is the signature header present? - Body size — is the payload truncated or empty?
- Timestamp — how old is the event?
Common payload surprises:
- Test mode vs live mode objects have different fields
- API version differences change payload shape
- Expanded vs non-expanded objects (Stripe
?expand=parameter)
Step 4: Verify signatures
If your handler returns 400/401, signature verification is the first suspect:
# Check the signature header exists
curl -s "https://api.hookdeploy.dev/v1/endpoints/ID/requests/REQUEST_ID" \
-H "Authorization: Bearer hd_live_YOUR_KEY" | jq '.data.headers'
Common signature failures:
- Wrong secret (dev vs prod, CLI vs Dashboard)
- Parsed JSON body instead of raw bytes
- Modified body (middleware, gzip decompression)
- Expired timestamp (Stripe rejects > 5 min old)
Temporarily log the raw body hash and expected signature to compare:
console.log('body length:', rawBody.length);
console.log('sig header:', request.headers['stripe-signature']);
Step 5: Test with curl
Isolate your handler from the provider by sending a test POST:
curl -v -X POST "http://localhost:3000/webhooks/stripe" \
-H "Content-Type: application/json" \
-d '{"type":"test","data":{"object":{"id":"test"}}}'
If curl works but provider webhooks fail, the issue is in delivery (URL, tunnel, SSL) not handler logic.
Test HookDeploy capture independently:
curl -X POST "https://hookdeploy.dev/h/YOUR_SLUG" \
-H "Content-Type: application/json" \
-d '{"test": true}'
Expected response: {"request_id":"..."}
Step 6: Check your forward/tunnel
If using HookDeploy forwarding or ngrok:
- Is ngrok/cloudflared still running?
- Did the tunnel URL change? (ngrok free tier generates new URLs per session)
- Is your local server listening on the expected port?
- Check HookDeploy forward status:
forward_status_code,forward_error
Step 7: Check handler logs
With a confirmed delivery and valid payload, the bug is in your application code:
- Add structured logging at the top of your webhook handler
- Log event type, object ID, and processing result
- Check for unhandled promise rejections
- Verify database connections and external API calls within the handler
Return 200 before slow work when possible:
// Good: acknowledge fast, process async
response.status(200).send('OK');
await queue.add('process-webhook', event);
// Bad: Stripe waits while you write to 3 databases
await db1.insert(event);
await db2.sync(event);
await sendEmail(event);
response.status(200).send('OK'); // might timeout
Common error patterns
“It worked yesterday”
- Tunnel URL changed (ngrok free tier)
- Webhook secret was rotated
- Endpoint was paused or deleted
- Plan limit reached
“Works in test mode, fails in production”
- Different webhook secrets for test vs live endpoints
- Different signing secrets — check which endpoint the sender uses
“Handler runs twice”
- Provider retry because first attempt returned non-2xx or timed out
- Missing idempotency check — see retry strategies
“Empty body”
- Sender uses
application/x-www-form-urlencoded— checkquerypanel in HookDeploy - Body exceeded size limit — check 413 responses
Debugging checklist
- Event triggered at provider (delivery log exists)
- Request captured in HookDeploy (if using HookDeploy URL)
- HTTP response was 200 to sender
- Payload contains expected event type and fields
- Signature header present and verification passes
- Forward/tunnel is active (if using forward)
- Handler logs show event received
- Handler returns 200 within provider timeout (10–30 seconds)
Related guides
Related guides
- Testing Stripe webhooks with HookDeploy
Complete guide to capturing, inspecting, and debugging Stripe webhook events using HookDeploy, the Stripe CLI, and signature verification.
- Webhook security best practices
Signature verification for Stripe, GitHub, and Shopify. HMAC-SHA256, replay attacks, IP allowlisting, HTTPS, and secret rotation.
- Webhook retry strategies
How major providers retry failed webhooks, idempotency keys, handling duplicates, and why capture-before-process helps.