hooksig
v0.2.0
Published
Zero-dependency webhook signature verification for the edge. One typed API for Stripe, GitHub, Standard Webhooks (OpenAI, Anthropic, Supabase, Clerk, Resend), Shopify, Slack, Square, Twilio, PayPal, Cloudflare, and Vercel. Runs on Cloudflare Workers, Bun,
Maintainers
Readme
hooksig
Zero-dependency webhook signature verification for the edge. One typed API for Stripe, GitHub, Standard Webhooks (OpenAI, Anthropic, Supabase, Clerk, Resend), Shopify, Slack, Square, Twilio, PayPal, Cloudflare, and Vercel. Built only on WebCrypto, so it runs unchanged on Cloudflare Workers, Bun, Deno, and Node 20+.
npm i hooksigVerify
import { verify } from "hooksig";
const result = await verify("stripe", {
payload: rawBody, // RAW body. Never JSON.stringify(parsedBody).
headers: request.headers,
secret: env.STRIPE_WEBHOOK_SECRET,
toleranceSeconds: 300, // optional, default 300, 0 disables the timestamp check
});
if (result.verified) {
// result.timestamp is the signed unix time, when the provider sends one
} else {
// result.reason: "no_signature_match" | "timestamp_out_of_tolerance"
// | "missing_signature_header" | "malformed_signature_header"
// | "missing_timestamp" | "invalid_secret"
}Prefer throwing in a route handler:
import { assertVerified } from "hooksig";
await assertVerified("standardwebhooks", { payload, headers, secret });Explain (what the playground uses)
import { explain } from "hooksig";
const e = await explain("github", { payload, headers, secret });
// e.signedPayload, e.expectedSignature, e.providedSignatures, e.steps, e.matchNotes
- Pass the raw request body. On Workers use
await request.text()or.arrayBuffer()before parsing. - A wrong signature always returns
no_signature_match, even if the timestamp is also stale, so a forged body leaks nothing about the timestamp. - Comparison is constant-time.
MIT.
