Webhook Debugging and Testing
Webhooks fail silently, retry unexpectedly, and carry payloads you never see until something breaks in production. This guide walks through every failure mode and the step-by-step workflow to find and fix them fast.
TL;DR
- •Webhook bugs are hard because the sender controls the request — you can't step through it
- •Most failures are wrong URL, missing signature header, payload mismatch, or timeout
- •Capture the raw request first — inspect headers before touching the payload
- •Always return a 200 quickly; do async processing separately to avoid retries
- •Log every inbound event with its full headers, timestamp, and response code
Why Webhook Debugging Is Hard
With a normal API call your code initiates the request — you control the URL, headers, and body, and you can reproduce it instantly with curl. With a webhook, the flow is reversed. The sender (Stripe, GitHub, Shopify) fires the request when an event happens. You can't pause it, you can't step through it, and the first sign of failure is usually a silent error log buried in a dashboard you haven't opened yet.
Async behaviour
Webhooks fire in the background. There's no request/response cycle visible in your code. The failure happens at delivery time, which may be seconds — or minutes — after the triggering event.
Retries obscure root cause
Most senders retry on failure (5xx or timeout). By the time you notice a problem, your handler may have been called 3–10 times, each with a slightly different delivery timestamp. The original failure is already buried.
Hidden failures
A 200 response to the sender does not mean your handler succeeded. If you process the payload after returning 200, errors are invisible to the sender and won't trigger retries — even when they should.
Common Webhook Issues
The vast majority of webhook failures fall into five categories. Check these before diving into application logic.
Wrong Endpoint URL
The registered URL doesn't resolve, returns a redirect, or points to a different path. Test by sending a manual curl POST to the URL from a machine outside your network. Check for trailing slashes — most webhook senders treat /webhook and /webhook/ as different URLs.
Authentication / Signature Errors
The sender includes a signature header (e.g., Stripe's X-Stripe-Signature, GitHub's X-Hub-Signature-256). Your handler must verify this before processing. If your secret is wrong, mismatched, or the verification step is skipped, you either reject legitimate events or accept forged ones.
Payload Mismatch
The sender changed their payload schema — a field was renamed, nested, or added. Your handler expects checkout.completed but receives checkout.session.completed. Alternatively, a new event type fires that your handler doesn't recognize and returns a 500.
Missing or Wrong Headers
The sender expects a specific Content-Type (usually application/json) and your handler rejects it, or your handler expects Authorization that the sender doesn't include. Conversely, your firewall or load balancer strips custom headers before they reach your handler.
Timeout Failures
Most webhook senders have a hard timeout of 5–30 seconds. If your handler takes longer — due to database writes, downstream API calls, or synchronous email sending — the sender marks the delivery as failed and retries. Your handler may process the event multiple times.
Step-by-Step Debugging Workflow
Follow this sequence when a webhook isn't behaving as expected. Each step narrows the problem before you touch application code.
Send the webhook to an inspector
Before connecting the sender to your real endpoint, point it at a Webhook Tester URL. This captures the raw HTTP request — method, all headers, full body — without any application code running. You now have a ground truth for what the sender is actually sending.
Inspect headers
Check the Content-Type, the signature header (X-Stripe-Signature, X-Hub-Signature-256, etc.), and any custom headers the sender documents. Note the exact header names and values — they are case-sensitive.
Inspect the payload
Pretty-print the JSON body. Confirm the event type, the top-level structure, and the field names your handler references. Compare against the sender's documentation. Copy this example payload into your test suite.
Validate the response your handler returns
Point the sender at your real endpoint and check what HTTP status code it returns. A 200 means success. A 4xx means a client error (bad URL, auth failure, not found). A 5xx means your handler crashed. A timeout means your handler took too long.
Reproduce locally with curl
Use the captured headers and body to replay the exact request against your local server. This gives you a reproducible test case you can run without waiting for the sender to fire a real event.
Add structured logging at the handler entry point
Log the event type, delivery timestamp, a correlation ID (usually the X-Request-Id or equivalent), and the response status. This creates an audit trail for every delivery — essential for debugging retries.
Using Webhook Testing Tools
A webhook testing tool captures inbound HTTP requests on a temporary URL so you can see exactly what the sender delivers. This is the fastest way to diagnose integration problems without writing a single line of handler code.
Using the Webhook Tester
- 1Open the Webhook Tester — it generates a unique URL immediately.
- 2Copy the URL and paste it into your service's webhook configuration (Stripe Dashboard, GitHub repository settings, Shopify admin, etc.).
- 3Trigger a test event from the sender's dashboard.
- 4The Webhook Tester captures the full request: HTTP method, all headers, timestamp, and raw body.
- 5Inspect the signature header and compare it against the sender's docs. Confirm the payload structure.
- 6Copy the body and use it in your handler's unit tests.
For inspecting API responses and formatting JSON payloads, use the API Response Formatter. Paste the raw webhook body to pretty-print it, sort keys, and inspect nested structures. For query-string encoded payloads (some older systems), use the Query String Parser.
Logging and Retry Strategies
What to log
Log at the handler entry point before any processing. Capture:
- •Event type (e.g., payment.succeeded, push, order.fulfilled)
- •Delivery ID or idempotency key (use as correlation ID)
- •Received timestamp (ISO 8601, UTC)
- •Source IP (for allowlist validation)
- •Signature verification result (pass/fail)
- •Response status code returned to sender
- •Processing duration
Handling retries safely
Webhook senders retry on failure — often with exponential backoff over hours or days. Your handler must be idempotent: processing the same event twice should produce the same outcome, not duplicate data.
Store delivery IDs
Before processing, check if you've already handled this delivery ID. If yes, return 200 without reprocessing. Most senders include a unique delivery ID in headers (Stripe-Idempotency-Key, X-GitHub-Delivery).
Return 200 immediately
Acknowledge receipt with a 200 and enqueue the event. Do heavy processing — DB writes, API calls, emails — in a background job. This prevents timeouts and unintentional retries.
Use a dead-letter queue
For events that fail processing after retry, route them to a dead-letter queue. This preserves the payload for manual inspection and replay without losing the original event.
Alert on persistent failures
Set up an alert when a webhook event has been retried more than N times or when your handler error rate spikes. Silent failure is the most dangerous mode in webhook integrations.
Best Practices
Always verify signatures
Never process a webhook without verifying the HMAC signature. Unverified webhooks can be spoofed by anyone who knows your endpoint URL. Verify on the raw request body before parsing.
Use HTTPS endpoints only
Senders will not deliver to HTTP endpoints in production (Stripe, GitHub, Shopify all enforce this). Register your handler behind HTTPS with a valid certificate.
Subscribe selectively
Subscribe only to the event types your handler processes. Receiving unhandled events adds noise, increases load, and can cause unexpected 500s if your handler isn't defensive about unknown event types.
Include a replay mechanism
Most services let you replay failed deliveries from their dashboard. Design your handler to be replayable — store the raw payload before processing so you can re-run it locally if something goes wrong.
Test with real payloads from the sender
Sender documentation often lags behind their actual payload schema. Always capture a live test event and use it as your test fixture — not the documentation sample.
Monitor end-to-end, not just errors
Check that expected events arrive at the expected frequency, not just that no errors occurred. A silent failure (webhook misconfigured at the sender side) looks identical to zero errors in your handler logs.