The jwt.io of webhooks. One consistent reference for how every provider signs a webhook, a live playground that builds the signature in front of you, and a zero-dependency verifier SDK that runs on the edge.
Webhook signing is fragmented. Every provider uses a slightly different canonical string, encoding, header, and timestamp rule, and the failure mode is silent: your check just returns false and you lose an afternoon. hooksig makes the whole thing visible, then hands you one typed API to verify any of them.
hooksig/
├─ packages/sdk hooksig, zero-dep WebCrypto verifier (Workers, Bun, Deno, Node)
├─ packages/test-vectors shared signed vectors; the SDK and the Rust core both verify against them
├─ crates/hooksig-core Rust canonical-string + HMAC + step output, compiled to WASM for the playground
└─ apps/web the reference site + interactive playground (Astro + React island)
The playground is the hero: it becomes the page people link to. The SDK rides underneath as the "or just drop this in" conversion.
import { verify } from "hooksig";
const result = await verify("stripe", {
payload: rawBody, // the RAW request body, never parsed-then-re-stringified
headers: request.headers, // Fetch Headers, a plain object, or Node headers
secret: env.STRIPE_WEBHOOK_SECRET,
});
if (!result.verified) {
return new Response(`rejected: ${result.reason}`, { status: 400 });
}One API across stripe, github, and standardwebhooks (which covers OpenAI, Anthropic, Supabase, Clerk, and Resend). No dependencies, no Node built-ins, so it runs unchanged on Cloudflare Workers where the official Node SDKs cannot.
- SDK: working and tested. Ten providers. Builds (ESM + CJS + d.ts), typechecks clean, 53 tests over valid, tampered, stale-timestamp, multi-signature, wrong-url, and RSA vectors. Run
cd packages/sdk && node --test test/*.test.ts. - Responsive + accessible. Layout is
rem-based so text scales with the browser font-size setting; verified no horizontal overflow across 320 to 1280px and at a 22px base font.pnpm --filter @hooksig/web verify:uidrives all pages in a real headless browser. - Rust core: built and verified.
cargo testpasses the shared-vector suite with verdicts identical to the SDK, andwasm-pack build --target webproduces the WASM module that computes the same signatures byte for byte. The two implementations cannot drift. (PayPal's RSA is SDK-only; the WASM core is HMAC-only for now.) - Playground: scaffolded, builds, and functional. Astro prerenders one SEO page per provider; the React island runs on the SDK's
explain()today, and the WASM core swaps in behind the same call with no UI change.
The SDK needs only Node and pnpm. The web app's playground runs on the WASM core, so building or serving it also needs the Rust toolchain and wasm-pack (cargo install wasm-pack). The playground falls back to the pure-TS SDK at runtime if the WASM ever fails to load.
pnpm install
pnpm --filter hooksig test # run the verifier tests (no Rust needed)
pnpm --filter @hooksig/web dev # builds the WASM core, then serves the site
pnpm vectors # regenerate the shared test vectorsAll nineteen shipping: Stripe, GitHub, Standard Webhooks, Shopify, Slack, Square, Twilio, PayPal, Cloudflare, Vercel, Razorpay, Cashfree, Notion, Linear, Sentry, Zoom, Calendly, DocuSign, and MongoDB Atlas. Twilio, Vercel, and MongoDB Atlas use HMAC-SHA1 (Atlas signs the raw body, base64, in an X-MMS-Signature header). Razorpay and Cashfree are the Indian gateways: Razorpay signs the raw body like GitHub, Cashfree folds the timestamp in front of the body before hashing. Notion, Linear, and Sentry are GitHub-shaped (raw body, hex): Notion keeps the sha256= prefix, Linear and Sentry ship a bare digest. Zoom mirrors Slack, Calendly mirrors Stripe, and DocuSign mirrors Shopify (base64) but strips double-quotes from the HMAC secret and rotates keys across numbered headers. PayPal is asymmetric (RSA over transmission headers plus a CRC32 of the body) and is verified in the SDK only, since the WASM playground core is HMAC-only for now; its playground page runs on the SDK path. Each HMAC provider is a data-and-adapter add on a proven format.
- Correctness is existential. A verifier that ever accepts a forged signature is worse than nothing, so every provider ships with test vectors before it ships.
- Constant-time comparison, always. No early-out on the first differing byte.
- Sign the raw bytes. The SDK never re-serializes your body, and the docs hammer this because it is the number one cause of false failures.
MIT licensed.