hooksentinel

API Reference

Core types and exported functions from @hooksentinel/core, its provider, store, framework, and testing subpaths.

@hooksentinel/core

createWebhookHandler(config)

function createWebhookHandler<TEvent>(
  config: WebhookHandlerConfig<TEvent>
): WebhookHandler<TEvent>;

Builds the framework-agnostic verify → deduplicate → parse → handle pipeline. The returned WebhookHandler is passed to a framework adapter (toExpressHandler, toFastifyHandler, etc.) — it is not called directly in application code.

interface WebhookHandlerConfig<TEvent> {
  /** A provider adapter — stripe(), github(), generic(), etc. */
  provider: ProviderAdapter<TEvent>;

  /** Deduplication store. Omit to disable idempotency entirely (not recommended). */
  idempotency?: IdempotencyStore;

  /** Called once per new, verified event. */
  onEvent: (event: TEvent, ctx: HandlerContext) => Promise<void> | void;

  /**
   * Called instead of the default error response when verification,
   * parsing, or idempotency fails. Return a Response-shaped object
   * to override status/body; omit to use hooksentinel's defaults.
   */
  onError?: (error: HookSentinelError, ctx: HandlerContext) => Promise<ErrorResponse> | ErrorResponse | void;

  /** Max accepted body size in bytes. Default 1_000_000 (1 MB). */
  maxBodyBytes?: number;
}

interface HandlerContext {
  /** The provider-assigned event ID used for deduplication. */
  eventId: string;
  /** The provider name, e.g. "stripe". */
  provider: string;
  /** Raw request headers, lowercased. */
  headers: Record<string, string>;
  /** Unix timestamp (seconds) the request was received. */
  receivedAt: number;
}

interface ErrorResponse {
  status: number;
  body?: unknown;
}

HookSentinelError

Thrown internally and passed to onError; never needs to be constructed by application code.

class HookSentinelError extends Error {
  readonly code: HookSentinelErrorCode;
  readonly status: number;
  readonly retryable: boolean;
  readonly provider?: string;
}

type HookSentinelErrorCode =
  | 'invalid_signature'
  | 'missing_signature_header'
  | 'malformed_signature_header'
  | 'timestamp_out_of_tolerance'
  | 'missing_raw_body'
  | 'payload_too_large'
  | 'parse_error'
  | 'duplicate_event'
  | 'idempotency_store_error'
  | 'handler_error'
  | 'provider_verification_error';

See Errors for what triggers each code, its HTTP status, whether it's retryable, and the fix.

Providers — @hooksentinel/core

Each provider factory returns a ProviderAdapter<TEvent> for use as the provider option in createWebhookHandler. See Supported providers for the full config reference and signature scheme per provider.

function stripe(config: { secret: string; tolerance?: number }): ProviderAdapter<StripeEvent>;
function github(config: { secret: string }): ProviderAdapter<GitHubEvent>;
function shopify(config: { secret: string }): ProviderAdapter<ShopifyEvent>;
function standardWebhooks(config: { secret: string }): ProviderAdapter<StandardWebhookEvent>;
function slack(config: { signingSecret: string }): ProviderAdapter<SlackEvent>;
function discord(config: { publicKey: string }): ProviderAdapter<DiscordInteraction>;
function twilio(config: { authToken: string; url: string }): ProviderAdapter<TwilioEvent>;
function paddle(config: { secret: string }): ProviderAdapter<PaddleEvent>;
function generic(config: {
  secret: string;
  headerName: string;
  algorithm?: 'sha256' | 'sha1' | 'ed25519';
}): ProviderAdapter<unknown>;

@hooksentinel/core/stores

interface IdempotencyStore {
  has(eventId: string): Promise<boolean>;
  markProcessed(eventId: string, ttlSeconds?: number): Promise<void>;
  release(eventId: string): Promise<void>;
}

function memoryStore(config?: { maxSize?: number }): IdempotencyStore;

function redisStore(config: {
  url?: string;
  client?: unknown; // an ioredis instance
  ttlSeconds?: number;
}): IdempotencyStore;

function prismaStore(config: {
  client: unknown; // a PrismaClient instance
  model: string;
  ttlSeconds?: number;
  /** When true, hooksentinel checks has() but never calls markProcessed() — see the same-transaction recipe. */
  verifyOnly?: boolean;
}): IdempotencyStore;

Full usage for each, including the same-transaction recipe, is in Idempotency. Implement IdempotencyStore yourself to back dedup with anything else — DynamoDB, a plain SQL table without Prisma, etc.

Framework adapters

Each is exported from its own subpath so unused framework code is never bundled — see Bundle size.

// @hooksentinel/core/express
function toExpressHandler(handler: WebhookHandler): (req: Request, res: Response) => Promise<void>;

// @hooksentinel/core/fastify
function toFastifyHandler(handler: WebhookHandler): (req: FastifyRequest, reply: FastifyReply) => Promise<void>;

// @hooksentinel/core/nestjs
class HookSentinelModule {
  static forRoot(config: WebhookHandlerConfig<unknown>): DynamicModule;
  static forRootAsync(config: { imports?: unknown[]; inject?: unknown[]; useFactory: (...args: unknown[]) => WebhookHandlerConfig<unknown> }): DynamicModule;
}
function VerifiedWebhook(): MethodDecorator;
function WebhookEvent(): ParameterDecorator;

// @hooksentinel/core/next
function toRouteHandler(handler: WebhookHandler): (request: Request) => Promise<Response>;

// @hooksentinel/core/hono
function toHonoHandler(handler: WebhookHandler): (c: Context) => Promise<Response>;

// @hooksentinel/core/lambda
function toLambdaHandler(handler: WebhookHandler): APIGatewayProxyHandlerV2;

Usage and framework-specific gotchas (raw body middleware ordering, NestJS rawBody, Lambda's base64 body encoding) are covered in Frameworks.

@hooksentinel/core/testing

function createTestSigner(
  provider: 'stripe' | 'github' | 'shopify' | 'standardWebhooks' | 'slack' | 'discord' | 'twilio' | 'paddle' | 'generic',
  config: Record<string, unknown>, // same shape as the matching provider() config
): (
  payload: unknown,
  options?: { timestamp?: number },
) => { body: string; headers: Record<string, string> };

See Testing for full examples across Express, Fastify, Next.js, and idempotency/timestamp edge cases.

Last updated on

On this page