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

@outworx/hooks

v1.5.1

Published

Lightweight webhook monitoring SDK — know when your webhooks break

Readme

@outworx/hooks

Lightweight webhook monitoring SDK -- know when your webhooks break.

Zero dependencies. Under 5KB. Works with Next.js, Express, and Fastify.

Installation

npm install @outworx/hooks

Quick Start

1. Initialize

Add your API key once at app startup, or set the OUTWORX_HOOKS_API_KEY environment variable.

import { init } from "@outworx/hooks";

init({ apiKey: "your-api-key" });

Or via environment variable:

OUTWORX_HOOKS_API_KEY=your-api-key

2. Wrap Your Webhook Handler

Next.js (App Router)

// app/api/webhooks/stripe/route.ts
import { withWebhookMonitoring } from "@outworx/hooks/nextjs";

async function handler(req: Request) {
  const body = await req.json();
  // ... process webhook
  return new Response("OK", { status: 200 });
}

export const POST = withWebhookMonitoring(
  { provider: "stripe", eventTypeField: "type" },
  handler
);

Express

import express from "express";
import { withWebhookMonitoring } from "@outworx/hooks/express";

const app = express();

app.post(
  "/webhooks/stripe",
  withWebhookMonitoring({ provider: "stripe", eventTypeField: "type" }),
  (req, res) => {
    // ... process webhook
    res.json({ received: true });
  }
);

Fastify

import Fastify from "fastify";
import { withWebhookMonitoring } from "@outworx/hooks/fastify";

const fastify = Fastify();

fastify.register(
  withWebhookMonitoring({ provider: "stripe", eventTypeField: "type" })
);

fastify.post("/webhooks/stripe", async (request, reply) => {
  // ... process webhook
  return { received: true };
});

Silent drop detection (v1.5+)

The most common webhook bug nobody tells you about: your handler returns 200 OK, the provider thinks delivery succeeded, but your business logic short-circuited (early return inside an if, swallowed exception in a try/catch, missing await). Provider doesn't retry. Dashboard says success. State is wrong. Hours later, a customer notices.

Outworx 1.5 catches this. Opt in by setting requireProcessingMark and calling track.processed() once your handler actually finishes its work:

import { withWebhookMonitoring } from "@outworx/hooks/nextjs";

export const POST = withWebhookMonitoring(
  {
    provider: "stripe",
    signatureSecret: process.env.STRIPE_WEBHOOK_SECRET!,
    requireProcessingMark: true,    // ← opt in to silent-drop detection
  },
  async (req, { track }) => {
    const event = await stripe.webhooks.constructEventAsync(...);
    if (event.type !== "charge.succeeded") {
      // Handler returns 200 but we never call track.processed() —
      // dashboard flags this as a silent_drop in the rare case it was
      // unintended. (For genuinely-ignored events, add an explicit
      // `track.processed({ reason: "ignored" })` so the signal is clean.)
      return Response.json({ received: true });
    }
    await chargeCustomer(event.data.object);
    track.processed();              // ← explicit ack
    return Response.json({ received: true });
  }
);

You can also explicitly mark application-level failures (handler returns 200 to avoid retries, but flags the event as not actually processed):

try {
  await chargeCustomer(event.data.object);
  track.processed();
} catch (err) {
  track.failed(err.message);        // tagged as application failure
  return Response.json({ received: true }); // 200 — don't retry
}

Express

import { withWebhookMonitoring } from "@outworx/hooks/express";

app.post(
  "/webhooks/stripe",
  withWebhookMonitoring({
    provider: "stripe",
    signatureSecret: process.env.STRIPE_WEBHOOK_SECRET!,
    requireProcessingMark: true,
  }),
  async (req, res) => {
    await chargeCustomer(req.body);
    req.outworx?.track.processed();
    res.json({ received: true });
  }
);

Fastify

fastify.post("/webhooks/stripe", {
  config: { webhookProvider: "stripe", requireProcessingMark: true },
  handler: async (request, reply) => {
    await chargeCustomer(request.body);
    request.outworx?.track.processed();
    return { received: true };
  },
});

Signature failure diagnostics (v1.5+)

When signature verification fails, Outworx now tells you why. Each failure includes a stable reason code and a developer-facing hint, surfaced on the event detail page in the dashboard:

| Reason | Meaning | Fix | |---|---|---| | missing_header | Provider's signature header was absent | Confirm endpoint URL in provider dashboard | | malformed_header | Header was present but didn't match expected format | Check for proxies/middleware mutating headers | | timestamp_drift | Signed timestamp outside tolerance | Sync server clock (NTP) | | hmac_mismatch | Signature didn't verify against your secret | Wrong/rotated secret, or modified body | | parsed_body | Body looks like JSON.stringify output rather than raw bytes | Verify against the raw request body | | unsupported_provider | No built-in verifier for this provider | Use a custom signatureVerifier function | | verifier_threw | Verifier threw an exception | Check error_message on the event |

To use the structured form programmatically:

import { verifyStripeSignatureDetailed } from "@outworx/hooks/security";

const result = verifyStripeSignatureDetailed({
  rawBody, header: req.headers["stripe-signature"], secret,
});
if (!result.valid) {
  console.warn(`signature failed: ${result.reason} — ${result.hint}`);
}

The boolean form (verifyStripeSignature(...)) still works unchanged.

Local tunnel (outworx forward)

Develop against real provider webhooks without deploying. The CLI ships inside this package — no extra install:

npx outworx forward 3000
  outworx forward  →  http://localhost:3000
  Public URL:  https://hooks.outworx.io/t/8b3a9f1c2d4e5067
  Expires:     11/7/2026, 4:18:42 PM
  Press Ctrl+C to stop.

  16:18:51  POST /webhooks/stripe  200  42ms
  16:18:52  POST /webhooks/stripe  200  31ms

Point your provider (Stripe, GitHub, Shopify, …) at the printed Public URL. Inbound requests are forwarded to your local server, and the response your handler returns is relayed back to the provider.

# Auth — set OUTWORX_API_KEY or pass --api-key
export OUTWORX_API_KEY=sk_live_...
npx outworx forward http://localhost:3000

# Shorthands
npx outworx forward 3000              # → http://localhost:3000
npx outworx forward localhost:8080    # → http://localhost:8080

# Override the Outworx server (self-hosted / staging)
npx outworx forward 3000 --endpoint=https://hooks.staging.example.com

Sessions auto-expire after 24 hours. Hop-by-hop headers are stripped on both legs; the relayed response includes X-Outworx-Tunnel: <slug> so your handler can detect tunneled traffic if needed.

Configuration

init({
  apiKey: "your-api-key",
  endpoint: "https://hooks.outworx.io/api/ingest", // default
  debug: true, // log events to console
  timeout: 3000, // HTTP timeout in ms
  onError: (err) => console.error(err), // error callback
});

Track Options

Each adapter accepts TrackOptions:

| Option | Type | Default | Description | | ----------------- | ------------------------ | ------- | -------------------------------------------------- | | provider | string | -- | Webhook provider name (e.g., "stripe", "shopify") | | eventTypeHeader | string | -- | Header name to extract event type from | | eventTypeField | string | -- | Body field to extract event type from (e.g., "type")| | captureBody | boolean | false | Capture request + response bodies (disabled by default for privacy) | | captureHeaders | boolean | true | Capture request headers (sensitive ones redacted) | | metadata | Record<string, unknown>| -- | Custom metadata attached to every event | | signatureSecret | string | -- | Auto-verify signature (see below) | | signatureVerifier| function | -- | Custom verifier function | | rejectInvalidSignatures | boolean | true | Respond with 401 when verification fails | | signatureTolerance | number | 300 | Replay-attack window (seconds) for timestamp providers | | idempotencyKey | function | -- | Extract a dedup key from the request (see below) | | idempotencyTtl | number | 86400 | TTL (seconds) for idempotency cache. Max 604800 (7d) |

Signature Verification

Built-in support for verifying webhook signatures for Stripe, GitHub, Shopify, Svix / Clerk, and Slack. When you provide a signatureSecret, the SDK:

  1. Computes the expected HMAC-SHA256 signature
  2. Compares it with the one in the request header (timing-safe)
  3. Rejects replay attacks (for timestamp-based providers)
  4. Returns 401 Invalid webhook signature without calling your handler if invalid
  5. Reports signature_valid to your Outworx dashboard for every request

Quick start

// Next.js (App Router) — works out of the box
import { init } from '@outworx/hooks';
import { withWebhookMonitoring } from '@outworx/hooks/nextjs';

init({ apiKey: process.env.OUTWORX_HOOKS_API_KEY! });

export const POST = withWebhookMonitoring(
  {
    provider: 'stripe',
    signatureSecret: process.env.STRIPE_WEBHOOK_SECRET!,
  },
  async (req) => {
    const body = await req.json();
    // Signature has already been verified — handle the event
    return Response.json({ received: true });
  }
);

Express — preserve the raw body

Express parses the body before your handler runs, so we need to stash the raw bytes first:

import express from 'express';
import { withWebhookMonitoring } from '@outworx/hooks/express';

const app = express();

app.use(express.json({
  verify: (req, _res, buf) => {
    (req as any).rawBody = buf.toString('utf8');
  },
}));

app.post(
  '/webhooks/stripe',
  withWebhookMonitoring({
    provider: 'stripe',
    signatureSecret: process.env.STRIPE_WEBHOOK_SECRET!,
  }),
  (req, res) => res.json({ received: true })
);

Fastify — use fastify-raw-body

import Fastify from 'fastify';
import rawBody from 'fastify-raw-body';
import { webhookMonitoringPlugin } from '@outworx/hooks/fastify';

const app = Fastify();
app.register(rawBody);
app.register(webhookMonitoringPlugin);

app.post('/webhooks/stripe', {
  config: {
    webhookProvider: 'stripe',
    signatureSecret: process.env.STRIPE_WEBHOOK_SECRET,
  },
  handler: async (_req, reply) => reply.send({ received: true }),
});

Custom verifier (any provider)

For providers not in the built-in list, or to plug in your own logic:

withWebhookMonitoring(
  {
    provider: 'my-service',
    signatureVerifier: async (rawBody, headers) => {
      // return true if valid, false otherwise
      return myCheck(rawBody, headers['x-my-signature']);
    },
  },
  handler
);

Standalone verifiers

Import and call the per-provider verifiers directly:

import {
  verifyStripeSignature,
  verifyGithubSignature,
  verifyShopifySignature,
  verifySvixSignature,
  verifySlackSignature,
} from '@outworx/hooks/security';

const valid = verifyStripeSignature({
  rawBody,
  header: req.headers['stripe-signature'] as string,
  secret: process.env.STRIPE_WEBHOOK_SECRET!,
});

Idempotency

Webhook providers retry deliveries when your handler times out or returns a non-2xx response — which can cause you to double-process the same event (double-charge the customer, send duplicate emails, etc.). Pass an idempotencyKey function and the SDK will short-circuit retries with the cached response from the first successful delivery.

export const POST = withWebhookMonitoring(
  {
    provider: 'stripe',
    signatureSecret: process.env.STRIPE_WEBHOOK_SECRET!,
    idempotencyKey: (_req, body) => (body as any).id,  // Stripe event ID
  },
  async (req) => {
    // Handler runs at most once per event ID, regardless of retries.
    const body = await req.json();
    await chargeCustomer(body);
    return Response.json({ received: true });
  }
);

On a duplicate delivery (same key, within idempotencyTtl), the SDK returns the cached response (status code + body) without calling your handler. The event still appears in your dashboard tagged as a duplicate.

Recommended keys per provider

// Stripe — event ID in the body
idempotencyKey: (_req, body) => (body as any).id

// GitHub — delivery ID header
idempotencyKey: (_req, _body, headers) => headers['x-github-delivery']

// Shopify — webhook ID header
idempotencyKey: (_req, _body, headers) => headers['x-shopify-webhook-id']

// Svix / Clerk — message ID header
idempotencyKey: (_req, _body, headers) => headers['svix-id']

Returning null or undefined from the function skips idempotency for that request.

Failure behavior

If our backend is unreachable when the SDK tries to check for duplicates, the check fails open — your handler runs as normal. We never let an idempotency failure block webhook delivery.

If your handler throws or returns 5xx, the idempotency key is not committed, so the next retry is free to re-run the handler. Stale reservations older than 30 seconds are automatically cleared.

Dashboard

View your webhook activity at hooks.outworx.io.

License

Business Source License 1.1 — see LICENSE for full text.

Free to use in production with the Outworx Hooks service. You may not use this SDK or any derivative work to offer a hosted webhook monitoring, analytics, or alerting service that competes with Outworx. The license converts to Apache 2.0 on 2030-04-17.

For commercial licensing inquiries, contact [email protected].