
Product
Microsoft Teams Notifications Are Now Available in Socket
Socket can now send alerts and supply chain attack notifications to Microsoft Teams, with filters that route the right updates to each channel.
anyhook-verify
Advanced tools
Verify Anyhook-Signature webhook deliveries — Web Crypto, zero runtime deps, works in Node / Bun / Deno / Cloudflare Workers / Vercel Edge.
Verify Anyhook-Signature webhook deliveries. Zero runtime deps. Same import in Node, Bun, Deno, Cloudflare Workers, and Vercel Edge — everything goes through Web Crypto.
Apache 2.0 · npm i anyhook-verify
import { verifyWebhook } from "anyhook-verify";
export async function POST(req: Request) {
const ok = await verifyWebhook(req, process.env.ANYHOOK_SIGNING_SECRET!);
if (!ok) return new Response("invalid signature", { status: 401 });
const event = await req.json();
// ... your handler logic runs here, can take 45+ seconds
return new Response("ok", { status: 200 });
}
That's it. The signing secret is the per-destination value AnyHook gives you in the dashboard — copy it into your app's environment as ANYHOOK_SIGNING_SECRET (or any name you like).
Every call to verifyWebhook rejects unless all of these pass:
Anyhook-Signature header (case-insensitive match)t=<unix_seconds>,v1=<hex>[,v1=<hex>...]v1 matches HMAC-SHA256(secret, "${timestamp}.${rawBody}")The body is read via req.clone().text() — your handler can still call req.json() afterwards. But if you've already consumed the body before calling verifyWebhook, req.clone() would throw — verifyWebhook detects this and returns false (the throwing variant returns reason: "body-already-consumed"). In that case use verifyPayload with the raw body string instead.
If you prefer to throw on bad signatures instead of remembering to check the boolean:
import { verifyWebhookOrThrow, WebhookVerificationError } from "anyhook-verify";
export async function POST(req: Request) {
try {
const { payload, timestamp } = await verifyWebhookOrThrow(
req,
process.env.ANYHOOK_SIGNING_SECRET!
);
const event = JSON.parse(payload); // already verified
// ...
return new Response("ok");
} catch (err) {
if (err instanceof WebhookVerificationError) {
// err.reason is one of:
// "missing-header" | "malformed-header" |
// "timestamp-outside-tolerance" | "signature-mismatch" |
// "body-already-consumed"
return new Response(err.message, { status: 401 });
}
throw err;
}
}
WebhookVerificationError.reason is a fixed string union — branch on it for logging or metrics without parsing the message text.
// app/api/webhooks/anyhook/route.ts
import { verifyWebhook } from "anyhook-verify";
export async function POST(req: Request) {
if (!(await verifyWebhook(req, process.env.ANYHOOK_SIGNING_SECRET!))) {
return new Response("invalid", { status: 401 });
}
const event = await req.json();
await handle(event);
return new Response("ok");
}
import { Hono } from "hono";
import { verifyWebhook } from "anyhook-verify";
const app = new Hono<{ Bindings: { ANYHOOK_SIGNING_SECRET: string } }>();
app.post("/webhooks/anyhook", async (c) => {
if (!(await verifyWebhook(c.req.raw, c.env.ANYHOOK_SIGNING_SECRET))) {
return c.text("invalid", 401);
}
const event = await c.req.json();
await handle(event);
return c.text("ok");
});
export default app;
Express's default JSON middleware consumes the body before your handler sees it, which breaks signature verification because the re-serialised JSON has different whitespace from what was signed. Two options — pick one:
A. Use express.raw() on the webhook route only, and pass the raw string to verifyPayload:
import express from "express";
import { verifyPayload, WebhookVerificationError } from "anyhook-verify";
const app = express();
// Important: NO express.json() before this route. The raw bytes must
// reach verifyPayload exactly as AnyHook signed them.
app.post(
"/webhooks/anyhook",
express.raw({ type: "application/json" }),
async (req, res) => {
const ok = await verifyPayload({
payload: req.body.toString("utf8"),
header: req.get("Anyhook-Signature") ?? null,
secret: process.env.ANYHOOK_SIGNING_SECRET!,
});
if (!ok) return res.status(401).send("invalid");
const event = JSON.parse(req.body.toString("utf8"));
await handle(event);
res.send("ok");
}
);
B. Mount JSON middleware with a verify callback to keep the raw bytes around. This is more involved — usually option A is simpler.
verifyWebhook(req, secret, options?): Promise<boolean>| Argument | Type | |
|---|---|---|
req | Request | Anything that implements the Web Fetch Request interface |
secret | string | Per-destination signing secret from the AnyHook dashboard |
options.tolerance | number | Replay window in seconds. Default 300 (5 min) |
options.now | number | Override wall clock (UNIX seconds). For tests |
Returns true only if every check passes. Never throws on malformed input — returns false.
verifyWebhookOrThrow(req, secret, options?): Promise<VerifiedWebhook>Same checks as verifyWebhook, but throws WebhookVerificationError on any failure and returns { payload: string; timestamp: number } on success. Use this if you'd rather rely on the throw than remember to check the boolean.
verifyPayload({ payload, header, secret, tolerance?, now? }): Promise<boolean>String-body version of verifyWebhook. Use when your framework has already consumed the body (Express raw body, queue-replayed events, etc.).
verifyPayloadOrThrow({ ... }): Promise<VerifiedWebhook>Throwing variant of verifyPayload.
parseSignatureHeader(value): { timestamp, signatures } | nullExposed for advanced use cases — logging, multi-tenant routing, custom verification flows. Returns null for any malformed input — never throws.
class WebhookVerificationError extends ErrorThrown by the *OrThrow variants. Carries a typed reason field — see Loud-failure variant above for the full enum.
anyhook-verify/testingFor your own integration tests, import the signing helper from the dedicated sub-export:
import { signWebhook } from "anyhook-verify/testing";
import { verifyWebhook } from "anyhook-verify";
it("processes a stripe.payment_intent.succeeded event", async () => {
const body = JSON.stringify({ type: "payment_intent.succeeded", id: "pi_test" });
const header = await signWebhook({
secret: process.env.ANYHOOK_SIGNING_SECRET!,
timestamp: Math.floor(Date.now() / 1000),
payload: body,
});
const req = new Request("http://localhost/webhook", {
method: "POST",
headers: { "Anyhook-Signature": header },
body,
});
// ... call your real handler with `req`
});
signWebhook is intentionally not in the main export — using it in production code is a smell (it forges deliveries with whatever secret you give it).
Verifying a webhook signature is ~20 lines of code. You could write it yourself. The reasons to use this package anyway:
node:crypto)v1= segments are checked, supporting overlap during a rotationIf you'd rather inline, the algorithm is described in src/verify.ts — the wire format is Stripe-compatible (t=<unix_seconds>,v1=<hex>).
Anyhook-Signature: t=1716567890,v1=a3f2…b7e8
t — UNIX seconds when the forwarder signed the delivery. Re-signed fresh on every retry, so replays-of-old-attempts always fail the tolerance window.v1= — hex HMAC-SHA256 of "${timestamp}.${rawBody}" using the destination's signing secret. Multiple v1= entries (during key rotation) are tried in order; any match passes.verifyPayload, key order / whitespace differences will break the signature. Always use the raw string the network gave you. verifyWebhook handles this automatically by reading from the Request directly — but only if you haven't already consumed the body.Apache-2.0 © AnyHook
FAQs
Verify Anyhook-Signature webhook deliveries — Web Crypto, zero runtime deps, works in Node / Bun / Deno / Cloudflare Workers / Vercel Edge.
We found that anyhook-verify demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Product
Socket can now send alerts and supply chain attack notifications to Microsoft Teams, with filters that route the right updates to each channel.

Security News
pnpm 12 rewrites the package manager in Rust, cutting install times by up to 90% while preserving pnpm 11 workflows and lockfiles.

Security News
Socket CTO Ahmad Nassri joins AppSec leaders at Black Hat to discuss active malware, package manager risks, and software supply chain defense.