Debug webhooks without the plumbing
Last updated:
Every webhook integration begins with the same unglamorous problem: the provider’s server needs to call your code, your code runs on your laptop, and nothing can reach your laptop. So before you can debug the actual work — the signature check, the payload shape, the handler logic — you first build plumbing: a tunnel, a staging deploy, a public URL that forwards to your machine. This article is about skipping that step with a temporary receiver such as this site’s Webhook Tester: create an address, point the provider at it, trigger the event, and read exactly what arrived. Then write your handler against the truth, not the documentation.
The debugging loop, and where it breaks
The healthy webhook loop is short: an event happens in the provider, the provider POSTs a signed payload to your endpoint, your endpoint verifies the signature, parses the body, does the work and answers 200. The unhealthy loop — the one everyone recognises — adds a fog around every step. The provider’s dashboard says delivery failed with a 500, or a 404, or nothing at all. Was that your code answering, a proxy in between, or an endpoint URL with a typo? The retry arrives with the same event id — or is it a new event for the same payment? The docs show a tidy JSON example, but production payloads carry extra fields, nested objects the example flattened, and occasionally a content type the example never mentioned. Stripe’s and GitHub’s webhook documentation (linked below) both stress the same disciplines — verify signatures, answer quickly, handle duplicates — and none of them can be practised until you can see a real delivery.
The break is almost never in the interesting logic. It is in the gap between what you believe was sent and what was sent: a signature header computed over a body your framework had already parsed and re-serialised, a secret copied from the wrong dashboard, an event type your switch statement spells slightly differently, a handler that works for the test event in the dashboard and not for the real event from production. Every one of those is invisible from inside your own logs, because your logs start after your code has already made its first wrong assumption. A captured delivery, recorded by something that is not your code, is the ground truth that cuts the loop short.
What a temporary receiver buys you
A temporary receiver is a public URL whose whole job is to accept anything and show it to you: the Webhook Tester on this site creates one in a click, keeps it for 30 minutes, records up to 50 deliveries with their method, path and query, headers and body (up to 256 KB each), and then deletes everything automatically. What that buys you is sequencing. Instead of deploying a half-written handler to a public box just to observe traffic — and then debugging the deployment and the handler at the same time — you separate observation from implementation. First, watch several real events arrive and learn their actual shape. Then write the handler locally, against captured payloads you can replay into your own tests by hand. Finally, point the provider at the real endpoint and trigger the event once more, now knowing exactly what a correct delivery looks like and what your handler should do with it.
It also buys you evidence when the conversation crosses a team boundary. “Your webhook sends the amount as a string” is an argument; a captured delivery showing "amount": "2000" is a fact. The same capture answers the support-ticket questions providers ask — which event id, which timestamp, which signature header was present — without a round of log archaeology, and it settles whether a missing delivery was never sent or was sent to the old test address someone forgot to update. Neighbouring tool, same philosophy: the HTTP status chain tracer shows what a URL does from a server’s vantage point when the question is redirects rather than payloads.
The workflow, with this tool
One: open the Webhook Tester and create a hook. Copy the receive URL — and from this moment treat that URL as a credential, for reasons the next section spells out. Two: paste it into the provider’s webhook settings as a test endpoint — Stripe, GitHub and most providers allow several endpoints, so your production endpoint stays untouched — or into your own service’s configuration if you are debugging a sender you control. Three: trigger the event the honest way. Use the provider’s real flow (a test-mode payment, an actual push to a scratch branch) rather than only the dashboard’s “send test event” button, because test events sometimes differ from real ones in exactly the fields you care about. Four: read the capture. Check the method and content type first, then the signature and event headers, then the body as raw text before you trust any pretty-printed view of it.
Five: write or fix your handler against that capture. Verify the signature over the raw body exactly as received — this is why the capture shows the unparsed bytes — and make your handler idempotent on the event id you can now see is stable across retries. Six: delete the hook (or let its 30 minutes run out), remove the test endpoint from the provider, and re-point delivery to your real handler. Seven: trigger the event once more and watch your own endpoint answer it. If it fails now, the difference between the captured good delivery and your handler’s behaviour is the whole bug, and it is a difference you can inspect instead of guess.
Safety: the URL is the credential
A receiver that anyone can read needs no login, and that convenience has a price you should pay attention to: the hook id in the URL is the only credential, and it authorises both sending and viewing. Anyone who obtains the URL — from a screenshot, a chat log, a shared screen — can read every payload the hook captures while it lives. So the rules are the password rules: never post the URL publicly, never use it for a live production feed, and delete the hook when the session ends rather than letting captures sit out their half hour. Payloads deserve the same respect from the other side: a real payment or push payload can contain customer email addresses, order details and metadata you would not paste into a public gist, and a captured delivery is exactly that kind of document. Test mode, scratch repositories and synthetic events exist precisely so that debugging artefacts contain nothing worth stealing.
The tester’s own limits are part of the safety story and are stated plainly on the tool page: captures are stored on the checker server only until the hook expires or you delete it — 30 minutes at most — bodies are capped at 256 KB and hooks at 50 requests, and the receiver never fetches, follows or replays anything, so a captured payload cannot turn into outbound traffic from the server. That still leaves the judgement calls with you. If a payload would be a problem in a stranger’s hands, it does not belong in a shared viewer at all: use your own infrastructure for that debugging session, the way you would for production secrets. Convenience tools earn their place on the low-sensitivity work that fills most debugging days.
Limits: receiving and inspecting only
What this tool deliberately does not do is everything after inspection. It does not forward deliveries to your laptop (that is a tunnel’s job, and tunnels are the right tool once the handler exists and you are iterating on it live), it does not replay a captured request to your real endpoint, it does not transform or sign anything, and it does not keep a history beyond 30 minutes. Those absences are the design: a receiver that forwards is a proxy with your traffic flowing through it, a receiver that replays is a sender, and both are larger trusts than a 30-minute in-tray. Use the tester for the phase it fits — seeing the truth before and while you build the handler — and let your own endpoint, behind your own authentication and logging, take over the moment the integration goes live.