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

stay-hooked

v0.1.0

Published

Unified webhook ingestion library — type-safe verification and parsing for 19+ SaaS providers

Downloads

11

Readme

stay-hooked

Unified webhook ingestion for TypeScript. Type-safe verification and parsing for 19+ SaaS providers with a consistent API.

  • Zero dependencies — uses only Node.js built-in crypto
  • Tree-shakable — import only the providers you need
  • Type-safe — fully typed event payloads per provider
  • Framework-agnostic — works with Express, Fastify, Next.js, Hono, NestJS, or plain Node.js

Installation

npm install stay-hooked

Quick Start

import { createWebhookHandler } from "stay-hooked";
import { stripe } from "stay-hooked/providers/stripe";

const handler = createWebhookHandler(stripe, {
  secret: process.env.STRIPE_WEBHOOK_SECRET!,
});

// In your HTTP handler:
const event = handler.verifyAndParse(headers, rawBody);

if (event.type === "checkout.session.completed") {
  console.log(event.data.customer_email); // fully typed
}

The Problem

Every SaaS sends webhooks differently:

// Before stay-hooked — repetitive, error-prone, untyped

// Stripe: HMAC-SHA256 with timestamp
const sig = req.headers["stripe-signature"];
const parts = sig.split(",");
const timestamp = parts.find(p => p.startsWith("t="))...
const hmac = crypto.createHmac("sha256", secret).update(`${ts}.${body}`)...

// GitHub: HMAC-SHA256 with sha256= prefix
const sig = req.headers["x-hub-signature-256"];
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(body)...

// Shopify: HMAC-SHA256 base64-encoded
const sig = req.headers["x-shopify-hmac-sha256"];
const expected = crypto.createHmac("sha256", secret).update(body).digest("base64")...

// Clerk: Svix-based with whsec_ prefix, base64 secret, multiple signature versions...

The Solution

// After stay-hooked — one API for everything

import { createWebhookHandler } from "stay-hooked";
import { stripe } from "stay-hooked/providers/stripe";
import { github } from "stay-hooked/providers/github";
import { shopify } from "stay-hooked/providers/shopify";
import { clerk } from "stay-hooked/providers/clerk";

// Same API for every provider
const stripeHandler = createWebhookHandler(stripe, { secret: STRIPE_SECRET });
const githubHandler = createWebhookHandler(github, { secret: GITHUB_SECRET });
const shopifyHandler = createWebhookHandler(shopify, { secret: SHOPIFY_SECRET });
const clerkHandler = createWebhookHandler(clerk, { secret: CLERK_SECRET });

// Each returns a typed WebhookEvent
const event = stripeHandler.verifyAndParse(headers, rawBody);

Framework Adapters

Express

import express from "express";
import { expressAdapter } from "stay-hooked/adapters/express";
import { stripe } from "stay-hooked/providers/stripe";

const app = express();

app.post(
  "/webhooks/stripe",
  express.raw({ type: "application/json" }),
  expressAdapter(stripe, {
    secret: process.env.STRIPE_WEBHOOK_SECRET!,
    onEvent: async (event) => {
      switch (event.type) {
        case "checkout.session.completed":
          await handleCheckout(event.data);
          break;
        case "invoice.payment_failed":
          await handleFailedPayment(event.data);
          break;
      }
    },
  })
);

Next.js App Router

// app/api/webhooks/stripe/route.ts
import { nextjsAdapter } from "stay-hooked/adapters/nextjs";
import { stripe } from "stay-hooked/providers/stripe";

export const POST = nextjsAdapter(stripe, {
  secret: process.env.STRIPE_WEBHOOK_SECRET!,
  onEvent: async (event) => {
    console.log(event.type, event.data);
  },
});

Fastify

import { fastifyAdapter } from "stay-hooked/adapters/fastify";
import { github } from "stay-hooked/providers/github";

fastify.post(
  "/webhooks/github",
  fastifyAdapter(github, {
    secret: process.env.GITHUB_WEBHOOK_SECRET!,
    onEvent: async (event) => {
      if (event.type === "push") {
        console.log(event.data.commits);
      }
    },
  })
);

Hono

import { Hono } from "hono";
import { honoAdapter } from "stay-hooked/adapters/hono";
import { shopify } from "stay-hooked/providers/shopify";

const app = new Hono();

app.post(
  "/webhooks/shopify",
  honoAdapter(shopify, {
    secret: process.env.SHOPIFY_WEBHOOK_SECRET!,
    onEvent: async (event) => {
      if (event.type === "orders/create") {
        console.log(event.data.total_price);
      }
    },
  })
);

NestJS

import { Controller, Post, UseGuards, Req } from "@nestjs/common";
import { createWebhookGuard, getWebhookEvent } from "stay-hooked/adapters/nestjs";
import { stripe } from "stay-hooked/providers/stripe";

const StripeGuard = createWebhookGuard({
  provider: stripe,
  secret: process.env.STRIPE_WEBHOOK_SECRET!,
});

@Controller("webhooks")
export class WebhooksController {
  @Post("stripe")
  @UseGuards(StripeGuard)
  handleStripe(@Req() req: Request) {
    const event = getWebhookEvent(req);
    console.log(event.type, event.data);
  }
}

Supported Providers

| Category | Provider | Verification Method | |---|---|---| | Payments | Stripe | HMAC-SHA256 + timestamp | | | Shopify | HMAC-SHA256 (base64) | | | PayPal | Header validation* | | | Square | HMAC-SHA256 (base64) | | | Paddle | HMAC-SHA256 + timestamp | | | LemonSqueezy | HMAC-SHA256 (hex) | | DevOps | GitHub | HMAC-SHA256 (sha256= prefix) | | | GitLab | Token comparison | | | Bitbucket | HMAC-SHA256 (sha256= prefix) | | | Linear | HMAC-SHA256 (hex) | | | Jira | Header validation | | Communication | Slack | HMAC-SHA256 (versioned) | | | Discord | Ed25519 | | | Twilio | HMAC-SHA1 (base64) | | | SendGrid | Header validation | | | Postmark | Header validation | | | Resend | Svix (HMAC-SHA256) | | Auth | Clerk | Svix (HMAC-SHA256) | | Infrastructure | Svix | HMAC-SHA256 (base64) |

*PayPal requires API-based verification for full security. The library validates required headers are present.

Verification notes

Some providers do not use cryptographic (HMAC) signatures. For these, stay-hooked validates that required headers are present, but cannot verify the payload was sent by the genuine provider without an additional API call:

| Provider | Reason | |---|---| | PayPal | Uses OAuth-based webhooks; cryptographic verification requires a separate API call | | Twilio | Uses HMAC-SHA1 but requires the full request URL to compute — provide it via options if available | | SendGrid | Uses a signed public-key scheme; header-presence check only in this release | | Jira | Sends a shared x-hub-signature but many plans omit it; header-presence check only | | Postmark | No signature scheme; validates x-postmark-token header is present |

For production use with these providers, consider adding your own secondary validation (e.g., re-fetching the resource from the provider's API).

Error Handling

stay-hooked throws specific error types you can catch:

import {
  WebhookVerificationError,
  WebhookParseError,
} from "stay-hooked";

try {
  const event = handler.verifyAndParse(headers, rawBody);
} catch (error) {
  if (error instanceof WebhookVerificationError) {
    // Signature verification failed
    console.error(error.provider); // "stripe"
    console.error(error.code);     // "SIGNATURE_MISMATCH"
  }
  if (error instanceof WebhookParseError) {
    // Payload parsing failed
    console.error(error.provider);
    console.error(error.code);     // "PARSE_FAILED"
  }
}

API Reference

createWebhookHandler(provider, options)

Creates a configured webhook handler.

const handler = createWebhookHandler(provider, { secret: "..." });
const event = handler.verifyAndParse(headers, rawBody);

WebhookEvent

The return type of verifyAndParse():

interface WebhookEvent<TEventMap> {
  type: string;       // Event type (e.g., "checkout.session.completed")
  data: unknown;      // Typed event payload
  raw: unknown;       // Raw parsed JSON
  timestamp?: Date;   // Event timestamp (if provided)
  id?: string;        // Event ID (if provided)
}

WebhookProvider

Interface implemented by every provider:

interface WebhookProvider<TEventMap> {
  name: string;
  verify(secret: string, headers: Record<string, string>, rawBody: string): void;
  parse(headers: Record<string, string>, rawBody: string): WebhookEvent<TEventMap>;
}

Requirements

  • Node.js >= 18
  • TypeScript >= 5.0 (for type safety)

License

MIT