DEV Community

Cover image for Testing a webhook you can't see is the whole problem
Samer Alsayegh for Draftbase

Posted on Originally published at draftbase.co AI-assisted

Testing a webhook you can't see is the whole problem

Building a webhook endpoint takes ten minutes. Making it survive retries, duplicates, and out-of-order delivery is the real work — and it's the half almost every tutorial skips, because in a tutorial delivery always works on the first try.

A webhook endpoint is a normal POST route that verifies a signature, returns 200 immediately, then does the real work after. Get those three details wrong and the bugs don't show up in dev — they show up in production, get blamed on the sender, and are much harder to explain after the fact.

The signature check is the most common failure. It's an HMAC hash of the exact bytes the sender signed. Parse the body as JSON first — which most frameworks do by default — and you re-serialize it before hashing: different key order, different whitespace, different bytes. Every signature fails, and the error looks exactly like a wrong secret. Keep the raw body. And compare with crypto.timingSafeEqual, not === — a plain string compare stops at the first wrong character, which leaks the correct signature one byte at a time.

Returning 200 fast matters because senders time out after a few seconds and retry on failure. A handler that does three service calls before responding gets retried mid-request. The 200 means "I have it," not "I finished."

Three ways to test without deploying: paste a capture URL from webhook.site into the sender to see real headers and body (vendor docs and vendor payloads disagree more than they should); tunnel localhost with ngrok http 3000 for live breakpoints; or save a captured payload and replay it with curl plus a computed HMAC signature — the one worth automating, since it needs no network and no sender.

Then test what actually breaks: send the same payload twice and check for double side effects (delivery is at-least-once, not exactly-once); add a deliberate delay before responding and watch the sender retry into your still-running handler; send a garbage signature, then no signature header at all — plenty of handlers check "does it match" and skip "does it exist." The fix for duplicates is idempotency: store every processed event id behind a unique database constraint, and keep those records longer than the sender's retry window.

Full breakdown — the Express code, the out-of-order-event test, and the four failures that actually show up in production: https://draftbase.co/webhooks-integration/how-to/how-to-create-and-test-a-webhook

More on this in r/draftbase_cms: https://reddit.com/r/draftbase_cms

Draftbase fires webhooks on every entry event, so you can test all of this against real ones. Hobby is free, Startup is $49/mo.

Top comments (0)