Skip to content
Webhooks SDK
Esc
navigateopen⌘Jpreview

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

~90 providers across 9 signature families →

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
  • 401 bad signature · 400 malformed · 500 handler 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.fetch

The uninteresting parts, done correctly

A verification bug looks exactly like "the webhook didn't fire". These are the details the SDK exists for.

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.fetch
Routing guide →

Tests 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)
Testing guide →

Mount it anywhere

The native interface is a Web-standard(Request) => Response— adapters cover the platforms that don't speak it.

Web-standardexport const POST = handler.fetch
Next.jsexport const { POST } = toNextRoute(handler)
Honoapp.post('/hook', toHonoHandler(handler))
Expressapp.post('/hook', raw, toExpressHandler(handler))
Node httpcreateServer(toNodeHandler(handler))

Framework adapters →

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.