Debugging webhook deliveries step by step
A practical checklist for webhooks that never arrive, arrive twice, time out or fail validation.
9 min read ยท Updated 24 October 2026
Webhook bugs are awkward because the failing request crosses an organizational boundary. You cannot attach a debugger to the sender, and the sender cannot see your logs. The way through is to split the path into stages and confirm each one in order: is the event sent, does it reach you, does your server answer in time, and does your code handle what arrived.
1. Is the provider sending anything?
Start on the provider's side. Most providers show a delivery log per webhook: the time, the response code they received, and often the request and response bodies. If there is no entry for the event you expected, the problem is configuration rather than networking. Common causes:
- The webhook is subscribed to different event types than the one you triggered.
- You triggered the event in test mode but registered the URL in live mode, or the other way around.
- The webhook was disabled automatically after repeated failures. Many providers do this, and it is easy to miss.
To separate "not sent" from "sent but lost", temporarily register a URL from the Tapwire inspector as an extra destination. If the event shows up there, the provider is sending and the problem is between the network and your code.
2. Does the request reach your server?
If the provider's log shows a failure such as a timeout or a connection error, check the path from the internet to your process.
- DNS: does the hostname resolve publicly, not only on your office network or VPN?
- TLS: is the certificate valid, unexpired and complete? An incomplete certificate chain often works in browsers, which can fill in missing intermediates, but fails for server-to-server clients that do not.
- Firewalls and allowlists: is the provider's traffic allowed? If you restrict by IP, confirm the provider's published address ranges are current.
- Redirects: many webhook senders do not follow redirects, and those that do may turn a
POSTinto aGETon a301or302, dropping the body. A missing or extra trailing slash, or an automatic redirect from http to https, is a classic cause. Register the final URL exactly.
3. Does your server answer in time?
Senders wait a limited time for a response. If your handler is slow, the sender records a timeout and retries, even if your code eventually finished successfully. That is how one event becomes several.
The fix is structural: verify the request, persist it or enqueue it, return 200 or 202, then process it in the background. To see how a provider treats a slow endpoint before it happens in production, give a Tapwire endpoint a response delay and watch the provider's delivery log.
4. Are you returning the right status code?
Providers decide whether to retry based on your status code. Any 2xx means "received". Most providers treat 4xx and 5xx as failures and retry them, and many also treat 3xx as a failure. Two consequences follow:
- If your framework returns
500because of an unrelated bug in post-processing, the provider retries an event you already handled. Idempotency protects you. - If you return
200for a request you could not process (for example, a payload that failed validation), the provider will never resend it. Return an error when you want a retry and log the reason.
5. Is signature verification failing?
Signature failures are almost always one of these:
- The body was parsed and re-serialized before verification, so the bytes changed.
- The wrong secret: test versus live, or an old secret after rotation.
- The comparison uses the wrong encoding, such as a hex digest compared with a base64 value, or with the prefix still attached.
- The timestamp tolerance is too strict and your server clock has drifted.
Capture one real delivery in the inspector, copy it as cURL, and run your verification code against that exact request locally. If it fails there, the problem is in your code, not the network. The signature guide walks through a correct implementation.
6. Duplicates and ordering
Duplicate deliveries are normal, not a provider bug. Store the event or delivery ID with a unique constraint and skip events you have already processed. If you process an event in several steps, make each step safe to repeat. For ordering, compare a version number or timestamp in the payload with what you have stored, or fetch the latest state of the object from the provider's API instead of trusting the event body.
7. The payload is not what you expected
Documentation examples are illustrations. Real payloads include optional fields, nulls, nested objects that are sometimes expanded and sometimes only an ID, and fields added after the docs were written. Capture a real event, read the raw body, and build your types from it. See working with JSON payloads for the usual traps.
8. Testing against a local server
Providers cannot reach localhost. You have three options: expose your local server through a tunnel, deploy to a test environment, or capture the delivery somewhere public and replay it. With Tapwire you can capture an event, then either copy it as cURL and run it locally, or use the Replay button to send it from your browser to your local server. Browser replay is subject to CORS, so your local server needs to allow the Tapwire origin, or you can use cURL, which has no such restriction.
Quick reference
| Symptom | Check first |
|---|---|
| No delivery in the provider log | Event subscription, test or live mode, webhook disabled |
| Provider shows a timeout | Slow handler; respond first, process later |
| Provider shows a TLS or connection error | Certificate chain, DNS, firewall |
| Request arrives with an empty body | A redirect turned POST into GET |
| Same event processed twice | Missing idempotency check on the event ID |
| Signature never matches | Raw body, secret, encoding, clock |