npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

hono-webhook-verify

v0.3.5

Published

Webhook signature verification middleware for Hono. Supports Stripe, GitHub, Slack, Shopify, Twilio, LINE, Discord, Standard Webhooks, and custom providers.

Readme

hono-webhook-verify

npm version CI License: MIT CodeRabbit Pull Request Reviews Devin Wiki

Webhook signature verification middleware for Hono. Verify webhooks from any provider with one line.

Works on Cloudflare Workers, Deno, Bun, Node.js, and any platform that supports the Web Crypto API.

Supported Providers

| Provider | Signature Header | Algorithm | |----------|-----------------|-----------| | Stripe | Stripe-Signature | HMAC-SHA256 + timestamp | | GitHub | X-Hub-Signature-256 | HMAC-SHA256 | | Slack | X-Slack-Signature | HMAC-SHA256 + timestamp | | Shopify | X-Shopify-Hmac-Sha256 | HMAC-SHA256 (base64) | | Twilio | X-Twilio-Signature | HMAC-SHA1 + URL + params | | LINE | X-Line-Signature | HMAC-SHA256 (base64) | | Discord | X-Signature-Ed25519 | Ed25519 | | Standard Webhooks | webhook-signature | HMAC-SHA256 (svix-compatible) | | Custom | Any | defineProvider() |

Installation

npm install hono-webhook-verify
# or
pnpm add hono-webhook-verify
# or
bun add hono-webhook-verify

Quick Start

import { Hono } from "hono";
import { webhookVerify } from "hono-webhook-verify";
import { stripe } from "hono-webhook-verify/providers/stripe";

import type { WebhookVerifyVariables } from "hono-webhook-verify";

const app = new Hono<{ Variables: WebhookVerifyVariables }>();

app.post(
  "/webhooks/stripe",
  webhookVerify({
    provider: stripe({ secret: process.env.STRIPE_WEBHOOK_SECRET! }),
  }),
  (c) => {
    const payload = c.get("webhookPayload");
    const rawBody = c.get("webhookRawBody");
    const provider = c.get("webhookProvider"); // "stripe"
    return c.json({ received: true });
  },
);

export default app;

Providers

Stripe

import { stripe } from "hono-webhook-verify/providers/stripe";

webhookVerify({
  provider: stripe({
    secret: "whsec_...",
    tolerance: 300, // optional: timestamp tolerance in seconds (default: 300)
  }),
});

GitHub

import { github } from "hono-webhook-verify/providers/github";

webhookVerify({
  provider: github({ secret: "your-webhook-secret" }),
});

Slack

import { slack } from "hono-webhook-verify/providers/slack";

webhookVerify({
  provider: slack({
    signingSecret: "your-signing-secret",
    tolerance: 300, // optional: timestamp tolerance in seconds (default: 300)
  }),
});

Shopify

import { shopify } from "hono-webhook-verify/providers/shopify";

webhookVerify({
  provider: shopify({ secret: "your-webhook-secret" }),
});

Twilio

import { twilio } from "hono-webhook-verify/providers/twilio";

webhookVerify({
  provider: twilio({ authToken: "your-auth-token" }),
});

LINE

import { line } from "hono-webhook-verify/providers/line";

webhookVerify({
  provider: line({ channelSecret: "your-channel-secret" }),
});

Discord

import { discord } from "hono-webhook-verify/providers/discord";

webhookVerify({
  provider: discord({ publicKey: "your-ed25519-public-key-hex" }),
});

Standard Webhooks (svix-compatible)

import { standardWebhooks } from "hono-webhook-verify/providers/standard-webhooks";

webhookVerify({
  provider: standardWebhooks({
    secret: "whsec_...", // base64-encoded secret with optional whsec_ prefix
    tolerance: 300, // optional: timestamp tolerance in seconds (default: 300)
  }),
});

Custom Provider

Use defineProvider() with the built-in crypto utilities to create a provider for any webhook source:

import {
  defineProvider,
  webhookVerify,
  hmac,
  fromHex,
  timingSafeEqual,
} from "hono-webhook-verify";

const myProvider = defineProvider<{ secret: string }>((options) => ({
  name: "my-service",
  async verify({ rawBody, headers }) {
    const signature = headers.get("X-My-Signature");
    if (!signature) {
      return { valid: false, reason: "missing-signature" };
    }
    const expected = await hmac("SHA-256", options.secret, rawBody);
    const received = fromHex(signature);
    if (!received || !timingSafeEqual(expected, received)) {
      return { valid: false, reason: "invalid-signature" };
    }
    return { valid: true };
  },
}));

app.post(
  "/webhooks/my-service",
  webhookVerify({ provider: myProvider({ secret: "..." }) }),
  (c) => c.json({ ok: true }),
);

Available crypto utilities: hmac, toHex, fromHex, toBase64, fromBase64, timingSafeEqual.

Context Variables

After successful verification, the middleware sets these variables on the Hono context:

| Variable | Type | Description | |----------|------|-------------| | webhookRawBody | string | The raw request body | | webhookPayload | unknown | Parsed JSON payload (or null if not JSON) | | webhookProvider | string | Provider name (e.g., "stripe", "github") |

For TypeScript, use the WebhookVerifyVariables type:

import type { WebhookVerifyVariables } from "hono-webhook-verify";

const app = new Hono<{ Variables: WebhookVerifyVariables }>();

Error Handling

By default, verification failures return a 401 response in RFC 9457 Problem Details format:

{
  "type": "https://hono-webhook-verify.dev/errors/missing-signature",
  "title": "Missing webhook signature",
  "status": 401,
  "detail": "Required webhook signature header is missing"
}

When hono-problem-details is installed, error responses are generated using its problemDetails().getResponse(). Otherwise, a built-in fallback is used. No configuration needed — detection is automatic.

Use the onError callback for custom error responses:

// Logging
webhookVerify({
  provider: stripe({ secret: process.env.STRIPE_WEBHOOK_SECRET! }),
  onError: (error, c) => {
    console.error("Webhook verification failed:", error.title, error.detail);
    return c.json({ error: "Invalid webhook" }, 401);
  },
});
// Custom error response with logging
webhookVerify({
  provider: stripe({ secret: process.env.STRIPE_WEBHOOK_SECRET! }),
  onError: (error, c) => {
    console.error("Webhook verification failed:", error.detail);
    return c.json({ error: error.title }, error.status as 400 | 401);
  },
});

Provider Auto-Detection

Use detectProvider() to identify the webhook source from request headers:

import { detectProvider } from "hono-webhook-verify";

const provider = detectProvider(request.headers);
// => "stripe" | "github" | "slack" | "shopify" | "twilio" | "line" | "discord" | "standard-webhooks" | null

Multi-Provider Endpoint

Handle multiple webhook providers on a single endpoint:

import { Hono } from "hono";
import { detectProvider, webhookVerify } from "hono-webhook-verify";
import type { WebhookVerifyVariables } from "hono-webhook-verify";
import { github } from "hono-webhook-verify/providers/github";
import { stripe } from "hono-webhook-verify/providers/stripe";

const providers = {
  stripe: stripe({ secret: process.env.STRIPE_WEBHOOK_SECRET! }),
  github: github({ secret: process.env.GITHUB_WEBHOOK_SECRET! }),
};

const app = new Hono<{ Variables: WebhookVerifyVariables }>();

app.post("/webhooks", async (c, next) => {
  const name = detectProvider(c.req.raw.headers);
  const provider = name ? providers[name as keyof typeof providers] : undefined;
  if (!provider) {
    return c.json({ error: "Unknown webhook provider" }, 400);
  }
  return webhookVerify({ provider })(c, next);
}, (c) => {
  const provider = c.get("webhookProvider");
  const payload = c.get("webhookPayload");
  console.log(`Received ${provider} webhook`);
  return c.json({ received: true });
});

Runtime Examples

Cloudflare Workers

import { Hono } from "hono";
import { webhookVerify } from "hono-webhook-verify";
import type { WebhookVerifyVariables } from "hono-webhook-verify";
import { stripe } from "hono-webhook-verify/providers/stripe";

type Bindings = { STRIPE_WEBHOOK_SECRET: string };

const app = new Hono<{ Bindings: Bindings; Variables: WebhookVerifyVariables }>();

app.post("/webhooks/stripe", (c, next) => {
  const middleware = webhookVerify({
    provider: stripe({ secret: c.env.STRIPE_WEBHOOK_SECRET }),
  });
  return middleware(c, next);
}, (c) => {
  return c.json({ received: true });
});

export default app;

Deno

import { Hono } from "npm:hono";
import { webhookVerify } from "npm:hono-webhook-verify";
import { github } from "npm:hono-webhook-verify/providers/github";

const app = new Hono();

app.post("/webhooks/github",
  webhookVerify({
    provider: github({ secret: Deno.env.get("GITHUB_WEBHOOK_SECRET")! }),
  }),
  (c) => c.json({ received: true }),
);

Deno.serve(app.fetch);

Bun

import { Hono } from "hono";
import { webhookVerify } from "hono-webhook-verify";
import { github } from "hono-webhook-verify/providers/github";

const app = new Hono();

app.post("/webhooks/github",
  webhookVerify({
    provider: github({ secret: Bun.env.GITHUB_WEBHOOK_SECRET! }),
  }),
  (c) => c.json({ received: true }),
);

export default app;

Troubleshooting

Signature verification fails

  • Check the secret format: Stripe uses whsec_... prefix. Standard Webhooks secrets are base64-encoded (with optional whsec_ prefix). Discord requires a hex-encoded Ed25519 public key.
  • Don't read the body before the middleware: webhookVerify reads c.req.text() internally. If another middleware consumes the body first, verification will fail because the raw body won't match the signature.
  • Environment variables: Ensure your secret is loaded correctly. An extra newline or whitespace in .env can cause mismatches.

Timestamp expired

  • Clock skew: Ensure your server's clock is synchronized (NTP). Providers like Stripe, Slack, and Standard Webhooks include timestamps and reject if the difference exceeds the tolerance (default: 300 seconds).
  • Increase tolerance: If your processing pipeline has high latency, increase the tolerance option:
    stripe({ secret: "whsec_...", tolerance: 600 }) // 10 minutes

Empty secret error

All providers validate that the secret is non-empty at construction time. If you see "<provider>: secret must not be empty", check that your environment variable is set and not undefined.

Twilio verification fails in production

Twilio signs the full request URL including the protocol and host. Behind a reverse proxy, c.req.url may report http:// instead of https://. Ensure your proxy sets the correct X-Forwarded-Proto header and your app reconstructs the correct URL.

Security

  • All signature comparisons use constant-time comparison (crypto.subtle.timingSafeEqual when available, XOR fallback otherwise)
  • Signatures are decoded to raw bytes before comparison to prevent timing leaks from string operations
  • Timestamp-based providers (Stripe, Slack, Standard Webhooks) reject expired signatures to prevent replay attacks
  • Discord uses Ed25519 asymmetric verification via crypto.subtle.verify

License

MIT