Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

12 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

hookproof logo

hookproof

The universal webhook signature verification library.
One uniform API to verify Stripe Β· GitHub Β· Slack Β· Shopify Β· Discord Β· Svix and more β€” zero dependencies, Web Crypto only, runs everywhere.

CI npm core size providers license

Live demo Β· Install Β· Quickstart Β· Providers Β· Frameworks Β· Contributing Β· License


hookproof interactive playground


Live demo

β†’ hookprooflive.vercel.app

Sign a webhook request in your browser and watch hookproof accept the genuine one and reject every tampered variant β€” a bad body, a wrong secret/key, an expired timestamp β€” with the exact error code. It runs 100% client-side (Web Crypto), which is only possible because the whole library is Web-standard and dependency-free.

Run it locally, or deploy your own copy in one click:

pnpm demo:build && npx serve demo   # http://localhost:3000

Deploy with Vercel

Why hookproof?

Hand-rolling webhook verification is easy to get subtly, dangerously wrong:

  • Timing attacks β€” comparing signatures with === leaks where they differ, so hookproof only compares decoded bytes through crypto.subtle.verify(...).
  • Replay β€” timestamped schemes (Stripe, Slack, Svix, …) are replayable unless you reject stale requests, so hookproof enforces a configurable window (default 300s) with an injectable clock.
  • The raw-body footgun β€” framework body parsers rewrite the bytes, so the body must be verified exactly as received. hookproof takes string | Uint8Array and never touches it before verifying.

One API handles all of it, for every provider.

Install

npm install hookproof
# or: pnpm add hookproof Β· bun add hookproof Β· deno add npm:hookproof

Quickstart

Three lines in a Next.js App Router route:

import { verifyWebhook } from "hookproof";

export async function POST(request: Request) {
  const webhook = await verifyWebhook("stripe", {
    payload: new Uint8Array(await request.arrayBuffer()), // raw bytes β€” never JSON.parse first
    headers: request.headers,
    secret: process.env.STRIPE_WEBHOOK_SECRET,
  });
  return Response.json({ received: true, id: webhook.id });
}

Prefer the non-throwing variant when you want to branch on the reason:

import { verifyWebhookSafe } from "hookproof";

const result = await verifyWebhookSafe("stripe", {
  payload: rawBody,
  headers: request.headers,
  secret: process.env.STRIPE_WEBHOOK_SECRET,
});

if (!result.ok) {
  // result.code is a stable, machine-readable string
  console.warn(`rejected: ${result.code}`);
} else {
  handle(result.webhook);
}

Every provider is also importable directly for tree-shaking:

import { verifyStripe } from "hookproof/stripe";
import { verifyGithub } from "hookproof/github";

Runtime support

Nothing here uses node:crypto β€” all crypto goes through globalThis.crypto.subtle.

Runtime Supported Notes
Node.js β‰₯ 20 βœ…
Bun βœ…
Deno βœ…
Cloudflare Workers βœ…
Vercel Edge βœ…
Ed25519 providers (Discord) ⚠️ Needs Web Crypto Ed25519 (Node β‰₯ 20, Deno, Bun, recent Workers); throws UnsupportedRuntimeError otherwise.

Providers

13 providers shipped.

Provider Status Scheme docs
clerk βœ… shipped docs
discord βœ… shipped docs
github βœ… shipped docs
lemonsqueezy βœ… shipped docs
meta βœ… shipped docs
paddle βœ… shipped docs
resend βœ… shipped docs
shopify βœ… shipped docs
slack βœ… shipped docs
standardwebhooks βœ… shipped docs
stripe βœ… shipped docs
svix βœ… shipped docs
twilio βœ… shipped docs

πŸ†“ Up for grabs (49) β€” paypal, square, adyen, mollie, razorpay, coinbase-commerce, wise, iyzico, paytr, gumroad, patreon, whop, gitlab, bitbucket, linear, jira, sentry, vercel, netlify, render, railway, supabase, hasura, contentful, sanity, strapi, algolia, typeform, tally, cal.com, calendly, zoom, intercom, zendesk, hubspot, mailgun, postmark, sendgrid, loops, customerio, brevo, klaviyo, workos, stytch, kinde, docusign, dropbox-sign, shippo, easypost. Claim one and add it in ~30 minutes.

Missing one? Add a provider in ~30 minutes.

Framework quickstarts

Cloudflare Workers and Deno use the native Request, so no adapter is needed β€” preserve the raw bytes with new Uint8Array(await request.arrayBuffer()).

Next.js (App Router)

import { verifyNextWebhook } from "hookproof/next";

export async function POST(request: Request) {
  try {
    const webhook = await verifyNextWebhook("github", request, {
      secret: process.env.GITHUB_WEBHOOK_SECRET,
    });
    return Response.json({ received: true, payload: webhook.payload });
  } catch {
    return new Response("invalid signature", { status: 400 });
  }
}

Express

Mount express.raw({ type: "*/*" }) on the webhook route so req.body stays a raw Buffer:

import express from "express";
import { verifyExpressWebhook } from "hookproof/express";

const app = express();

app.post("/webhooks/stripe", express.raw({ type: "*/*" }), async (req, res) => {
  try {
    const webhook = await verifyExpressWebhook("stripe", req, {
      secret: process.env.STRIPE_WEBHOOK_SECRET,
    });
    res.json({ received: true, id: webhook.id });
  } catch {
    res.status(400).send("invalid signature");
  }
});

Hono

import { Hono } from "hono";
import { verifyHonoWebhook } from "hookproof/hono";

const app = new Hono();

app.post("/webhooks/slack", async (c) => {
  try {
    await verifyHonoWebhook("slack", c, { secret: c.env.SLACK_SIGNING_SECRET });
    return c.json({ received: true });
  } catch {
    return c.text("invalid signature", 400);
  }
});

Cloudflare Workers

import { verifyWebhook } from "hookproof";

interface Env {
  GITHUB_WEBHOOK_SECRET: string;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    try {
      await verifyWebhook("github", {
        payload: new Uint8Array(await request.arrayBuffer()),
        headers: request.headers,
        secret: env.GITHUB_WEBHOOK_SECRET,
      });
      return new Response("ok");
    } catch {
      return new Response("invalid signature", { status: 400 });
    }
  },
};

Deno

import { verifyWebhook } from "hookproof";

Deno.serve(async (request: Request) => {
  try {
    await verifyWebhook("github", {
      payload: new Uint8Array(await request.arrayBuffer()),
      headers: request.headers,
      secret: Deno.env.get("GITHUB_WEBHOOK_SECRET"),
    });
    return new Response("ok");
  } catch {
    return new Response("invalid signature", { status: 400 });
  }
});

Error handling

verifyWebhook throws a WebhookVerificationError subclass on any failure; verifyWebhookSafe returns a discriminated result instead. Error codes are stable and machine-readable:

missing_header Β· malformed_header Β· invalid_signature Β· timestamp_out_of_tolerance Β· missing_secret Β· missing_public_key Β· missing_url Β· unsupported_provider Β· unsupported_runtime

Error messages never contain secret material or payload contents.

How it works

Providers are thin configurations over shared engines, so the whole library stays tiny and every provider behaves consistently:

  • HMAC engine β€” hash (SHA-1/256/512), encoding (hex/base64/base64url), header + prefix, custom key derivation, multi-signature, and an optional replay window. Covers most providers.
  • Svix / Standard Webhooks engine β€” {id}.{ts}.{body}, base64, whsec_ key derivation, v1, signature lists.
  • Ed25519 engine β€” asymmetric verification via crypto.subtle.verify("Ed25519", …) (Discord).
  • Bespoke β€” the rare provider that doesn't fit (Twilio signs the URL + params).

Adding a provider is a small config plus test vectors β€” see CONTRIBUTING.md.

Contributing

Your provider missing? Adding one takes about 30 minutes:

pnpm new-provider acme

The scaffold generates the provider, its test skeleton, and its README, and registers it β€” you fill in the scheme config and vectors. See CONTRIBUTING.md and the provider wishlist.

Security

Found a vulnerability? Please follow the responsible-disclosure process in SECURITY.md β€” do not open a public issue.

License

MIT Β© hookproof contributors

About

Just open-sourced hookproof πŸͺ β€” verify webhook signatures from Stripe, GitHub, Slack, Shopify, Discord and more with one API. Free forever (MIT). Zero deps. Timing-safe. Works on Node, Bun, Deno, Cloudflare Workers. Contributions welcome β€” 45+ providers on the backlog.

Resources

Code of conduct

Contributing

Security policy

Stars

7 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages