hooksig / Standard Webhooks
HMAC-SHA256 · base64 · timestamped
Standard Webhooks
The Svix spec. Learn it once and you can verify a whole wave of providers at once.
One adapter covers OpenAI, Anthropic, Supabase, Clerk, Resend.
Try it — live, in your browser
Request — edit anything
Verify this in code
import { verify } from "hooksig";
const result = await verify("standardwebhooks", {
payload: rawBody, // the RAW body, not parsed JSON
headers: request.headers,
secret: env.WEBHOOK_SECRET,
});
if (!result.verified) {
return new Response(result.reason, { status: 400 });
}no match — signature invalid
Signed string
HMAC-SHA256( secret, signed string ) = expected
Expected vs provided
expected
provided
How it's built
The scheme
- Header
webhook-signature - AlgorithmHMAC-SHA256
- Encodingbase64
- Timestampyes, so it has replay protection
- SignsHMAC-SHA256 over `{id}.{timestamp}.{rawBody}`, base64-encoded. The key is the base64-DECODED bytes of the whsec_ secret.
Gotchas
- The secret is base64. You must decode `whsec_...` to bytes before using it as the key. This is the #1 mistake.
- The signed content is `{webhook-id}.{webhook-timestamp}.{body}`.
- webhook-signature is a space-separated list of `v1,<sig>`. Match any.
- Check webhook-timestamp against your tolerance for replay protection.