Zero dependencies · Web Crypto only · Node 22+, Workers, Deno, Bun
Every webhook.
One handler.
webhooks-sdk verifies, parses, and routes webhooks from every provider. Signature schemes, replay windows, setup handshakes, and duplicate deliveries — the parts that fail quietly — handled once, correctly.
Shipping today — plus any Standard Webhooks vendor, no wrapper needed
Swap the provider.
Keep the handler.
Every provider invented its own signature scheme, replay window, and setup handshake. The differences are real but almost never interesting — so the SDK absorbs them behind one contract.
- ✓Signature verified against the exact bytes on the wire
- ✓Replay window enforced, secrets rotate as arrays
- ✓
401bad signature ·400malformed ·500handler threw, so the provider retries - ✓Typed, provider-native payloads — no lossy abstraction
import { createWebhookHandler } from 'webhooks-sdk'
import { stripe } from 'webhooks-sdk/stripe'
const handler = createWebhookHandler({
provider: stripe({ secret: env.STRIPE_SECRET }),
on: {
'payment_intent.succeeded': async (event) => {
await fulfill(event.payload.data.object)
},
},
})
export const POST = handler.fetchThe uninteresting parts, done correctly
A verification bug looks exactly like "the webhook didn't fire". These are the details the SDK exists for.
Verification done right
Constant-time comparison, replay windows, and multi-secret rotation — the subtle bugs that fail quietly, handled once.
Learn more →Handshakes answered
Signed and unsigned setup challenges answered in the right order — Discord's invalid-signature probe gets its 401.
Learn more →Idempotency built in
Providers deliver at-least-once. Duplicates are suppressed and acknowledged — plug in memory, Redis, or KV.
Learn more →The raw-body trap, avoided
Signatures cover the exact bytes on the wire. The SDK reads them once — and the adapters keep body parsers away.
Learn more →Test with real signatures
Every provider ships a signing helper, so tests exercise the actual verification path instead of mocking it.
Learn more →Runs everywhere
Zero dependencies — Web Crypto and fetch only. The same code on Node 22+, Cloudflare Workers, Deno, and Bun.
Learn more →One route, many providers
WebhookRouterserves any number of providers from a single endpoint — the provider is picked from the URL.
const router = new WebhookRouter({
providers: {
stripe: stripe({ secret: env.STRIPE_SECRET }),
github: github({ secret: env.GITHUB_SECRET }),
resend: resend({ secret: env.RESEND_SECRET }),
},
})
// app/api/webhooks/[provider]/route.ts
export const POST = router.fetchTests that test something
A test that mocks verification tests nothing. Sign fixtures with the real algorithm and run the whole pipeline.
import { createWebhookRequest } from 'webhooks-sdk/testing'
import { signStripeWebhook } from 'webhooks-sdk/stripe'
const request = createWebhookRequest({
body,
headers: {
'stripe-signature': await signStripeWebhook(body, secret),
},
})
// the real verification path — no mocks
await handler.process(request)Mount it anywhere
The native interface is a Web-standard(Request) => Response— adapters cover the platforms that don't speak it.
export const POST = handler.fetchexport const { POST } = toNextRoute(handler)app.post('/hook', toHonoHandler(handler))app.post('/hook', raw, toExpressHandler(handler))createServer(toNodeHandler(handler))9 families · ~90 providers
Most "bespoke" signature schemes aren't. Nine families cover almost every provider — and once a family ships, each new provider is a header name, an encoding, and a test fixture.