Here is the most boring 18 lines of code in any webhook-consuming codebase:
const sig = req.headers['stripe-signature'];
if (!sig) return res.status(400).end();
let event;
try {
event = stripe.webhooks.constructEvent(
req.rawBody,
sig,
process.env.STRIPE_WEBHOOK_SECRET,
);
} catch (err) {
console.error('Stripe signature verification failed:', err.message);
return res.status(400).end();
}
// ...handle event
It is boring, it is correct, and it is also wrong everywhere it appears in your codebase except the one service that copied it most recently.
Why webhook verification rots
Pick any team that has been consuming webhooks for more than a year. Open three random services. You will find, roughly:
- Service A uses the official Stripe SDK and verifies properly.
- Service B forgot to enable the
toleranceparameter, so a captured webhook can be replayed up to 24 hours later. - Service C silently skips verification when
NODE_ENV !== 'production', and someone shippedNODE_ENV=staginglast quarter. - Service D is internal and uses a custom HMAC scheme that compares hex strings with
===instead of constant-time, and the dev who wrote it has long since left.
Each of those is a one-line bug in code nobody is incentivised to look at. The webhook works. Events get processed. Until somebody discovers the gap.
The deeper issue: verification logic belongs at the trust boundary, not deep inside each service. Treating every internal service as if it has to redo authentication is a holdover from the days when there was no shared edge. There is now.
The shape of the answer
What we wanted: a single URL that customers point Stripe (or GitHub, or Slack, or whatever) at. That URL knows the secret, knows the format's wire spec, and verifies every request. Verified requests get forwarded to the customer's app with a header that says "trust me, this is real." Unverified requests either get dropped or marked clearly so the app can log them without acting on them.
That ingress is now live in KnoxCall under Inbound Webhooks. You give it:
- A name (used in the receiver URL:
hooks.knoxcall.com/<your-tenant-slug>/<name>). - A format: Stripe, GitHub, Slack, AWS-SNS-HMAC, KnoxCall legacy, or a custom-named header.
- The shared secret you pre-shared with the upstream provider.
- Optionally, a forward URL inside your infrastructure.
You paste the receiver URL into Stripe's webhook config. Done. Every event Stripe sends gets verified, audited, and forwarded with X-Knox-Verified: true.
Six formats, one verifier
Webhook formats are arbitrarily different. Stripe puts the timestamp inside the signature header (t=...,v1=...) and signs ${timestamp}.${body}. Slack uses two separate headers and signs v0:${timestamp}:${body}. GitHub signs the body alone with X-Hub-Signature-256. AWS SNS does base64. There is no underlying standard — every provider invented its own.
We implemented all six. Same internal verifier, different parsers per format. The output is a uniform { valid, reason?, timestamp_seconds? } shape.
The killer test that proves we got the wire format right: KnoxCall's outbound signing (the inverse direction) emits Stripe-format headers that Stripe's own SDK verifies end-to-end in our test suite. If stripe.webhooks.constructEvent accepts a header we generated, the format is byte-correct. The same round-trip exists for every format — we sign with our outbound code, then verify with our inbound code, and the property holds for all 31 unit tests we ship.
Replay protection at the edge
Formats with timestamps (Stripe, Slack) get an enforced replay window. Default is 300 seconds — Stripe's and Slack's own recommendation. A request whose timestamp is older than that gets rejected with replay_window_exceeded, even if the signature is otherwise valid.
This is the boring, easily-forgotten check that closes the most realistic webhook attack: an attacker who briefly has access to one historical webhook payload and tries to replay it later. Stripe enforces it; Slack enforces it; most ad-hoc verifying code does not, because the SDK call defaults to 0 (off) in some languages and the developer trusted the default.
You configure the window per subscription (range 0–86400 seconds). 0 disables it; only do that for formats without a timestamp where the check is meaningless anyway.
What you actually save
If you have one service consuming webhooks, this gets you:
- Audit: every event, verified or not, in API Logs.
signature_valid,signature_error,verified_count,failed_count. Filterable per webhook. - One-click rotation: rotate the secret in the admin UI. New secret displayed once. Old one stops working immediately. Update Stripe's config. Done.
- Deep-link to logs: "show me the last failed verifications for the Stripe webhook" is one click from the webhook detail page.
If you have seven services consuming webhooks, this is meaningfully bigger:
- Verification lives in one place. Drift can't develop.
- Adding webhook consumers stops adding HMAC code review.
- The trust boundary is where it belongs — at the edge, not seven independent locations.
What this is not
Inbound Webhooks is not a webhook router (we don't fan out one event to N consumers — your app does that if needed). It is not a webhook log/replay tool (we audit, but we don't store full bodies for replay). It is the edge verifier — the boring 18 lines, factored out of every service that consumes webhooks and run once per request.
If you do need a webhook router or a replay tool, you build it on top of this. The forward URL points at your existing service, which now does the boring verification once for free and gets to focus on the actual event handling.
Try it
If you're already on KnoxCall: Automation → Inbound Webhooks → New Webhook. Pick Stripe, paste your whsec_* secret, copy the receiver URL into the Stripe dashboard. The next event Stripe sends shows up in API Logs.
If you're not on KnoxCall: free tier includes one inbound webhook with up to 1,000 events per month. Plenty to verify a single Stripe integration end-to-end before deciding whether the rest of the platform makes sense for your team.
The deeper docs are at wiki/essentials/inbound-webhooks — including the wire-format guide for anyone integrating a custom provider.