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