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

@better-webhook/core

v0.11.4

Published

Core webhook handling functionality for better-webhook

Downloads

911

Readme

@better-webhook/core

npm npm monthly

Type-safe webhooks in TypeScript. Verified. Validated. Delightful.

Stop wrestling with raw webhook payloads. better-webhook gives you fully-typed event handlers, automatic signature verification, and schema validation—all with a beautiful, chainable API.

import { github } from "@better-webhook/github";
import { push } from "@better-webhook/github/events";

const webhook = github().event(push, async (payload) => {
  // ✨ payload is fully typed!
  console.log(`${payload.pusher.name} pushed to ${payload.repository.name}`);
});

Why better-webhook?

  • 🔒 Secure by default — HMAC signature verification out of the box
  • 📝 Fully typed — TypeScript autocomplete for every event payload
  • ✅ Schema validated — Zod validation catches malformed webhooks
  • 🔗 Chainable API — Register multiple handlers with elegant fluent syntax
  • 🎯 Framework adapters — First-class support for Next.js, Hono, Express, NestJS, GCP Cloud Functions

Installation

npm install @better-webhook/core
# or
pnpm add @better-webhook/core
# or
yarn add @better-webhook/core

Quick Start

The fastest way to get started is with a pre-built provider:

npm install @better-webhook/github @better-webhook/nextjs
// app/api/webhooks/github/route.ts
import { github } from "@better-webhook/github";
import { push, pull_request } from "@better-webhook/github/events";
import { toNextJS } from "@better-webhook/nextjs";

const webhook = github()
  .event(push, async (payload) => {
    console.log(`Push to ${payload.repository.full_name}`);
    console.log(`Commits: ${payload.commits.length}`);
  })
  .event(pull_request, async (payload) => {
    if (payload.action === "opened") {
      console.log(`New PR: ${payload.pull_request.title}`);
    }
  });

export const POST = toNextJS(webhook);

That's it. Your webhook endpoint is:

  • ✅ Verifying signatures (set GITHUB_WEBHOOK_SECRET env var)
  • ✅ Validating payloads against schemas
  • ✅ Fully typed with autocomplete
  • ✅ Handling multiple event types

If no secret is configured, requests are rejected by default.

Error Handling

Gracefully handle failures with the built-in error hooks:

const webhook = github()
  .event(push, async (payload) => {
    await deployToProduction(payload);
  })
  .onError((error, context) => {
    // Called when your handler throws
    console.error(`Failed to handle ${context.eventType}:`, error);
    console.error("Payload:", context.payload);

    // Send to your error tracking service
    Sentry.captureException(error, { extra: context });
  })
  .onVerificationFailed((reason, headers) => {
    // Called when signature verification fails
    console.warn("Webhook verification failed:", reason);
    console.warn("Headers:", headers);

    // Alert on potential attacks
    alertSecurityTeam({ reason, headers });
  });

Handler Context

Every event handler receives a second parameter—context—containing metadata about the webhook request. This is useful for logging, debugging, and accessing request details:

const webhook = github().event(push, async (payload, context) => {
  // Know which provider sent this webhook
  console.log(`Provider: ${context.provider}`); // "github"

  // Access the event type
  console.log(`Event: ${context.eventType}`); // "push"

  // Access the delivery ID (extracted from provider-specific headers)
  console.log(`Delivery ID: ${context.deliveryId}`);

  // Get all request headers (normalized to lowercase)
  console.log(`User-Agent: ${context.headers["user-agent"]}`);

  // Access the raw body for advanced use cases
  console.log(`Raw body length: ${context.rawBody.length}`);

  // Know when the webhook was received
  console.log(`Received at: ${context.receivedAt.toISOString()}`);

  await processWebhook(payload);
});

HandlerContext Properties

| Property | Type | Description | | ------------ | ----------------------------------- | --------------------------------------------------------------- | | eventType | string | The event type (e.g., "push", "order.created") | | provider | string | Provider name (e.g., "github", "stripe") | | deliveryId | string \| undefined | Unique delivery ID from provider headers (for logging/deduping) | | headers | Record<string, string\|undefined> | Normalized request headers (lowercase keys) | | rawBody | string | The raw request body as a string | | receivedAt | Date | Timestamp when the webhook was received |

Replay Protection (Built-In)

Use replay protection to enforce deduplication in core with a pluggable store. By default, duplicate deliveries return 409.

Replay protection now follows a lifecycle:

  1. Reserve replay key before schema/handler execution
  2. By default, commit the key only after a successful handled response (e.g. 200)
  3. Release the reservation for unhandled responses (e.g. 204) or processing failures (4xx/5xx)
import { createInMemoryReplayStore } from "@better-webhook/core";

const replayStore = createInMemoryReplayStore({
  maxEntries: 10000,
  cleanupIntervalMs: 60_000,
});

const webhook = github()
  .withReplayProtection({
    store: replayStore,
  })
  .event(push, async (payload) => {
    await processWebhook(payload);
  });

Use a shared store (for example Redis) in production so deduplication works across all app instances.

Custom Store (Bring Your Own)

If you want your own backend (Redis, SQL, DynamoDB, etc.), implement the atomic replay store contract:

import type { ReplayStore } from "@better-webhook/core";

class RedisReplayStore implements ReplayStore {
  async reserve(
    key: string,
    inFlightTtlSeconds: number,
  ): Promise<"reserved" | "duplicate"> {
    const result = await redis.set(key, "1", {
      NX: true,
      EX: inFlightTtlSeconds,
    });
    return result === "OK" ? "reserved" : "duplicate";
  }

  async commit(key: string, ttlSeconds: number): Promise<void> {
    await redis.expire(key, ttlSeconds);
  }

  async release(key: string): Promise<void> {
    await redis.del(key);
  }
}

const webhook = github()
  .withReplayProtection({
    store: new RedisReplayStore(),
  })
  .event(push, handler);

Optional Replay Freshness Policy

You can enforce a replay timestamp tolerance when providers expose signed timestamps:

const webhook = recall()
  .withReplayProtection({
    store: createInMemoryReplayStore(),
    policy: {
      ttlSeconds: 24 * 60 * 60,
      timestampToleranceSeconds: 5 * 60,
      key: (context) => {
        const candidate = context.replayKey ?? context.deliveryId;
        return candidate ? `${context.provider}:${candidate}` : undefined;
      },
    },
  })
  .event(bot_done, handler);

Provider timestamp support in this repository:

  • @better-webhook/resend: replay key (svix-id) with signed timestamp metadata
  • @better-webhook/recall: includes signed timestamp metadata
  • @better-webhook/github: replay key (x-github-delivery) only
  • @better-webhook/ragie: replay key (nonce) only

Manual Deduplication (Fallback)

If you do not enable replay protection, you can still dedupe manually with context.deliveryId:

const processedIds = new Set<string>();

const webhook = github().event(push, async (payload, context) => {
  if (context.deliveryId && processedIds.has(context.deliveryId)) {
    return;
  }
  if (context.deliveryId) {
    processedIds.add(context.deliveryId);
  }
  await processWebhook(payload);
});

Context in Multiple Handlers

All handlers for the same event receive the same context object:

const webhook = github()
  .event(push, async (payload, context) => {
    // Log the webhook
    await logger.info(`Received ${context.eventType}`, {
      provider: context.provider,
      receivedAt: context.receivedAt,
    });
  })
  .event(push, async (payload, context) => {
    // Both handlers receive the exact same context
    await processPayload(payload);
  });

Multiple Handlers

Register multiple handlers for the same event—they run sequentially:

const webhook = github()
  .event(push, async (payload) => {
    // First: Update database
    await db.commits.insertMany(payload.commits);
  })
  .event(push, async (payload) => {
    // Second: Send notifications
    await slack.notify(`New push to ${payload.repository.name}`);
  })
  .event(push, async (payload) => {
    // Third: Trigger CI/CD
    await triggerBuild(payload.after);
  });

Secret Management

Secrets are resolved automatically in this order:

  1. Explicit secret — Passed to the adapter
  2. Provider default — Set when creating the provider
  3. Environment variable{PROVIDER}_WEBHOOK_SECRET (e.g., GITHUB_WEBHOOK_SECRET)
  4. FallbackWEBHOOK_SECRET

Verification is required by default. If no secret can be resolved, the request is rejected unless verification is explicitly disabled.

Verification runs before unhandled-event routing, so unknown/unsupported events must still pass signature verification before a 204 is returned.

// Option 1: Environment variable (recommended)
// Set GITHUB_WEBHOOK_SECRET=your-secret
const webhook = github().event(push, handler);

// Option 2: Explicit secret
const webhook = github({ secret: "your-secret" }).event(push, handler);

// Option 3: At adapter level
export const POST = toNextJS(webhook, { secret: "your-secret" });

Request Body Size Guard

Use maxBodyBytes to reject oversized requests with 413 before parsing, signature verification, schema validation, and handler execution.

const webhook = github()
  .maxBodyBytes(1024 * 1024) // 1MB builder default
  .event(push, handler);

// Per-request override (adapters pass this through)
await webhook.process({
  headers: { "x-github-event": "push" },
  rawBody: payload,
  maxBodyBytes: 2 * 1024 * 1024, // 2MB
});

Use this as an app-layer guard. Keep proxy/framework request-size limits configured for earlier rejection and better memory protection.

Creating Custom Providers

Need to handle webhooks from a service we don't have a pre-built provider for? Create your own in minutes:

Quick Custom Webhook

For one-off integrations, use customWebhook:

import {
  customWebhook,
  createHmacVerifier,
  defineEvent,
  z,
} from "@better-webhook/core";

// Define your event schemas with Zod
const OrderSchema = z.object({
  orderId: z.string(),
  status: z.enum(["pending", "completed", "cancelled"]),
  amount: z.number(),
  customer: z.object({
    id: z.string(),
    email: z.string().email(),
  }),
});

const RefundSchema = z.object({
  refundId: z.string(),
  orderId: z.string(),
  amount: z.number(),
  reason: z.string().optional(),
});

// Create your webhook handler
const orderCreated = defineEvent({
  name: "order.created",
  schema: OrderSchema,
  provider: "my-ecommerce" as const,
});

const refundRequested = defineEvent({
  name: "refund.requested",
  schema: RefundSchema,
  provider: "my-ecommerce" as const,
});

const webhook = customWebhook({
  name: "my-ecommerce",
  // Tell us where to find the event type
  getEventType: (headers) => headers["x-webhook-event"],
  // Optional: Extract delivery ID for logging/deduplication
  getDeliveryId: (headers) => headers["x-delivery-id"],
  // Optional: Verify webhook signatures
  verify: createHmacVerifier({
    algorithm: "sha256",
    signatureHeader: "x-webhook-signature",
    signaturePrefix: "sha256=",
  }),
})
  .event(orderCreated, async (payload) => {
    // payload is typed as OrderSchema!
    console.log(`New order: ${payload.orderId}`);
    await sendConfirmationEmail(payload.customer.email);
  })
  .event(refundRequested, async (payload) => {
    // payload is typed as RefundSchema!
    console.log(`Refund requested: ${payload.refundId}`);
  });

Reusable Provider Package

Building a provider to share across your organization or publish to npm? Use createProvider:

import {
  createProvider,
  createWebhook,
  createHmacVerifier,
  defineEvent,
  z,
} from "@better-webhook/core";

// schemas.ts
export const PaymentSucceededSchema = z.object({
  id: z.string(),
  amount: z.number(),
  currency: z.string(),
  customer_email: z.string().email(),
});

export const PaymentFailedSchema = z.object({
  id: z.string(),
  amount: z.number(),
  currency: z.string(),
  error_code: z.string(),
  error_message: z.string(),
});

// provider.ts
const paymentSucceeded = defineEvent({
  name: "payment.succeeded",
  schema: PaymentSucceededSchema,
  provider: "payment-gateway" as const,
});

const paymentFailed = defineEvent({
  name: "payment.failed",
  schema: PaymentFailedSchema,
  provider: "payment-gateway" as const,
});

export interface PaymentGatewayOptions {
  secret?: string;
}

function createPaymentGatewayProvider(options?: PaymentGatewayOptions) {
  return createProvider({
    name: "payment-gateway",
    secret: options?.secret,
    getEventType: (headers) => headers["x-event-type"],
    getDeliveryId: (headers) => headers["x-request-id"],
    verify: createHmacVerifier({
      algorithm: "sha256",
      signatureHeader: "x-signature",
    }),
  });
}

// Public API - matches the pattern of built-in providers
export function paymentGateway(options?: PaymentGatewayOptions) {
  return createWebhook(createPaymentGatewayProvider(options));
}

// Usage is identical to built-in providers.
const webhook = paymentGateway({ secret: "sk_..." })
  .event(paymentSucceeded, async (payload) => {
    await fulfillOrder(payload.id);
    await sendReceipt(payload.customer_email);
  })
  .event(paymentFailed, async (payload) => {
    await flagPaymentFailure(payload.id, payload.error_code);
  });

Verification Helpers

HMAC Verification

Most webhook providers use HMAC signatures. We make it easy:

import { createHmacVerifier } from "@better-webhook/core";

// GitHub-style: sha256=<hex>
const githubVerifier = createHmacVerifier({
  algorithm: "sha256",
  signatureHeader: "x-hub-signature-256",
  signaturePrefix: "sha256=",
});

// Base64-encoded signatures
const base64Verifier = createHmacVerifier({
  algorithm: "sha256",
  signatureHeader: "x-signature",
  signatureEncoding: "base64",
});

// SHA-1 (for legacy systems)
const sha1Verifier = createHmacVerifier({
  algorithm: "sha1",
  signatureHeader: "x-signature",
});

Custom Verification Logic

For complex signature formats (like Stripe's t=timestamp,v1=signature), use the low-level verifyHmac:

import { verifyHmac } from "@better-webhook/core";

function stripeVerify(
  rawBody: string | Buffer,
  headers: Headers,
  secret: string,
): boolean {
  const signatureHeader = headers["stripe-signature"];
  if (!signatureHeader) return false;

  // Parse Stripe's format: t=1234567890,v1=abc123...
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((part) => part.split("=")),
  );

  const timestamp = parts["t"];
  const signature = parts["v1"];

  // Stripe signs: timestamp.payload
  const signedPayload = `${timestamp}.${rawBody}`;

  return verifyHmac({
    algorithm: "sha256",
    rawBody: signedPayload,
    secret,
    signature,
  });
}

Provider Without Verification

For development or trusted internal services, you must explicitly disable verification:

import { customWebhook, defineEvent, z } from "@better-webhook/core";

const UserSchema = z.object({
  id: z.string(),
  email: z.string().email(),
});

const userCreated = defineEvent({
  name: "user.created",
  schema: UserSchema,
  provider: "internal-service" as const,
});

const userDeleted = defineEvent({
  name: "user.deleted",
  schema: UserSchema,
  provider: "internal-service" as const,
});

const webhook = customWebhook({
  name: "internal-service",
  getEventType: (headers) => headers["x-event-type"],
  verification: "disabled",
})
  .event(userCreated, async (payload) => {
    await indexUser(payload.id);
  })
  .event(userDeleted, async (payload) => {
    await removeUser(payload.id);
  });

Observability

Add metrics, logging, and tracing to your webhook handlers with the built-in observability API. The observer pattern lets you subscribe to lifecycle events without modifying your handler code.

Quick Start with Stats

Use the built-in createWebhookStats() helper to track webhook metrics:

import { github } from "@better-webhook/github";
import { createWebhookStats } from "@better-webhook/core";
import { toNextJS } from "@better-webhook/nextjs";

const stats = createWebhookStats();

const webhook = github()
  .observe(stats.observer)
  .event(push, async (payload) => {
    console.log(`Push to ${payload.repository.name}`);
  });

export const POST = toNextJS(webhook);

// Get stats snapshot anytime
// stats.snapshot() returns:
// {
//   totalRequests: 150,
//   successCount: 145,
//   errorCount: 5,
//   byProvider: { github: { total: 150, success: 145, error: 5 } },
//   byEventType: { push: { total: 100, success: 98, error: 2 }, ... },
//   avgDurationMs: 23.5,
// }

Custom Observers

Create custom observers to integrate with your metrics/logging infrastructure:

import { github } from "@better-webhook/github";
import { type WebhookObserver } from "@better-webhook/core";

const metricsObserver: WebhookObserver = {
  onRequestReceived: (event) => {
    console.log(`[${event.provider}] Webhook received`);
  },

  onCompleted: (event) => {
    // Send to your metrics system (Prometheus, Datadog, etc.)
    metrics.histogram("webhook_duration_ms", event.durationMs, {
      provider: event.provider,
      eventType: event.eventType || "unknown",
      status: String(event.status),
      success: String(event.success),
    });

    metrics.increment("webhook_requests_total", {
      provider: event.provider,
      eventType: event.eventType || "unknown",
      status: String(event.status),
    });
  },

  onHandlerFailed: (event) => {
    // Log errors with context
    logger.error("Webhook handler failed", {
      provider: event.provider,
      eventType: event.eventType,
      handlerIndex: event.handlerIndex,
      error: event.error.message,
      durationMs: event.handlerDurationMs,
    });
  },

  onVerificationFailed: (event) => {
    // Alert on potential attacks
    alertSecurityTeam({
      reason: event.reason,
      provider: event.provider,
    });
  },
};

const webhook = github().observe(metricsObserver).event(push, handler);

Observer via Adapter Options

You can also add observers at the adapter level without modifying the webhook builder:

import { toNextJS } from "@better-webhook/nextjs";
import { createWebhookStats } from "@better-webhook/core";

const stats = createWebhookStats();

// Observer added at adapter level
export const POST = toNextJS(webhook, {
  observer: stats.observer,
});

Multiple Observers

Chain multiple observers for different purposes:

const webhook = github()
  .observe(stats.observer) // Track metrics
  .observe(loggingObserver) // Log events
  .observe(tracingObserver) // Add traces
  .event(push, handler);

// Or pass an array
const webhook = github()
  .observe([stats.observer, loggingObserver])
  .event(push, handler);

Lifecycle Events

Observers can subscribe to these lifecycle events:

| Event | Description | Key Fields | | ----------------------------- | ----------------------------------- | -------------------------------------------- | | onRequestReceived | Webhook request starts processing | provider, rawBodyBytes | | onJsonParseFailed | JSON parsing failed | error, durationMs | | onEventUnhandled | No handler for event type (204) | eventType, durationMs | | onBodyTooLarge | Body exceeds configured limit (413) | maxBodyBytes, rawBodyBytes | | onVerificationSucceeded | Signature verification passed | verifyDurationMs | | onVerificationFailed | Signature verification failed | reason, verifyDurationMs | | onSchemaValidationSucceeded | Zod schema validation passed | validateDurationMs | | onSchemaValidationFailed | Zod schema validation failed | error, validateDurationMs | | onHandlerStarted | Handler execution begins | handlerIndex, handlerCount | | onHandlerSucceeded | Handler completed successfully | handlerIndex, handlerDurationMs | | onHandlerFailed | Handler threw an error | error, handlerIndex, handlerDurationMs | | onCompleted | Processing complete (always called) | status, success, durationMs |

All events include common fields: provider, eventType, deliveryId, rawBodyBytes, startTime, receivedAt.

Recommended Metric Names

When integrating with metrics systems, we recommend these metric names:

// Counters
webhook_requests_total; // Labels: provider, eventType, status
webhook_errors_total; // Labels: provider, eventType, error_type

// Histograms
webhook_duration_ms; // Labels: provider, eventType, status
webhook_handler_duration_ms; // Labels: provider, eventType, handler_index
webhook_body_bytes; // Labels: provider

Observer Error Isolation

Observer errors are automatically caught and swallowed—they will never break your webhook processing:

const faultyObserver: WebhookObserver = {
  onCompleted: () => {
    throw new Error("Observer error"); // This won't break anything
  },
};

// Webhook still processes successfully even if observer throws
const webhook = github().observe(faultyObserver).event(push, handler);

API Reference

Functions

| Function | Description | | ----------------------------- | -------------------------------------------------- | | customWebhook(config) | Create a webhook builder with inline configuration | | createProvider(config) | Create a reusable provider instance | | createWebhook(provider) | Create a webhook builder from a provider | | createHmacVerifier(options) | Create an HMAC verification function | | verifyHmac(options) | Low-level HMAC verification | | createWebhookStats() | Create an in-memory stats collector with observer | | createInMemoryReplayStore() | Create an in-memory replay/idempotency store |

Types

interface ProviderConfig {
  name: string;
  secret?: string;
  verification?: "required" | "disabled";
  getEventType: (headers: Headers, body?: unknown) => string | undefined;
  getDeliveryId?: (headers: Headers) => string | undefined;
  getPayload?: (body: unknown) => unknown;
  getReplayContext?: (
    headers: Headers,
    body?: unknown,
  ) => { replayKey?: string; timestamp?: number };
  verify?: (
    rawBody: string | Buffer,
    headers: Headers,
    secret: string,
  ) => boolean;
}

interface HmacVerifyOptions {
  algorithm: "sha1" | "sha256" | "sha384" | "sha512";
  signatureHeader: string;
  signaturePrefix?: string;
  signatureEncoding?: "hex" | "base64";
}

interface HandlerContext {
  eventType: string;
  provider: string;
  deliveryId?: string;
  headers: Record<string, string | undefined>;
  rawBody: string;
  receivedAt: Date;
}

interface ErrorContext {
  eventType: string;
  deliveryId?: string;
  payload: unknown;
}

interface ReplayContext {
  provider: string;
  eventType?: string;
  deliveryId?: string;
  replayKey?: string;
  timestamp?: number;
}

type ReplayReserveResult = "reserved" | "duplicate";

interface AtomicReplayStore {
  reserve(
    key: string,
    inFlightTtlSeconds: number,
  ): Promise<ReplayReserveResult> | ReplayReserveResult;
  commit(key: string, ttlSeconds: number): Promise<void> | void;
  release(key: string): Promise<void> | void;
}

type ReplayStore = AtomicReplayStore;

interface ReplayPolicy {
  ttlSeconds: number;
  inFlightTtlSeconds?: number;
  timestampToleranceSeconds?: number;
  key(context: ReplayContext): string | undefined;
  onDuplicate?: "conflict" | "ignore";
}

// Event handler signature
type EventHandler<T> = (
  payload: T,
  context: HandlerContext,
) => Promise<void> | void;

// Enable replay protection on a webhook builder
webhook.withReplayProtection({
  store: replayStore,
  policy: optionalPolicy,
});

// Observer interface for webhook lifecycle events
interface WebhookObserver {
  onRequestReceived?: (event: RequestReceivedEvent) => void;
  onJsonParseFailed?: (event: JsonParseFailedEvent) => void;
  onEventUnhandled?: (event: EventUnhandledEvent) => void;
  onBodyTooLarge?: (event: BodyTooLargeEvent) => void;
  onVerificationSucceeded?: (event: VerificationSucceededEvent) => void;
  onVerificationFailed?: (event: VerificationFailedEvent) => void;
  onSchemaValidationSucceeded?: (event: SchemaValidationSucceededEvent) => void;
  onSchemaValidationFailed?: (event: SchemaValidationFailedEvent) => void;
  onHandlerStarted?: (event: HandlerStartedEvent) => void;
  onHandlerSucceeded?: (event: HandlerSucceededEvent) => void;
  onHandlerFailed?: (event: HandlerFailedEvent) => void;
  onReplaySkipped?: (event: ReplaySkippedEvent) => void;
  onReplayFreshnessRejected?: (event: ReplayFreshnessRejectedEvent) => void;
  onReplayReserved?: (event: ReplayReservedEvent) => void;
  onReplayDuplicate?: (event: ReplayDuplicateEvent) => void;
  onReplayCommitted?: (event: ReplayCommittedEvent) => void;
  onReplayReleased?: (event: ReplayReleasedEvent) => void;
  onCompleted?: (event: CompletedEvent) => void;
}

// Stats snapshot returned by createWebhookStats().snapshot()
interface WebhookStatsSnapshot {
  totalRequests: number;
  successCount: number;
  errorCount: number;
  byProvider: Record<string, { total: number; success: number; error: number }>;
  byEventType: Record<
    string,
    { total: number; success: number; error: number }
  >;
  avgDurationMs: number;
}

License

MIT