hooksentinel
Frameworks

Express

Integrate hooksentinel with Express — raw body handling, mounting, and error responses.

Install

npm install @hooksentinel/core

toExpressHandler is exported from @hooksentinel/core/express so Express itself never has to be a dependency of the core package.

The raw body requirement

Signature verification runs against the exact bytes the provider signed. If Express's express.json() middleware parses the body first, you've lost the raw bytes and every request will fail with missing_raw_body.

Scope express.raw() to the webhook route only, and mount it before any global express.json():

src/server.ts
import express from 'express';
import { toExpressHandler } from '@hooksentinel/core/express';
import { createWebhookHandler, stripe } from '@hooksentinel/core';
import { memoryStore } from '@hooksentinel/core/stores';

const app = express();

const stripeWebhook = createWebhookHandler({
  provider: stripe({ secret: process.env.STRIPE_WEBHOOK_SECRET! }),
  idempotency: memoryStore(),
  onEvent: async (event) => {
    if (event.type === 'checkout.session.completed') {
      await fulfillOrder(event.data.object.id);
    }
  },
});

app.post(
  '/webhooks/stripe',
  express.raw({ type: 'application/json' }),
  toExpressHandler(stripeWebhook),
);

// safe to parse JSON globally after the webhook route is registered
app.use(express.json());

async function fulfillOrder(sessionId: string) {
  // ...
}

app.listen(3000);

Multiple providers

Mount one route per provider, each with its own handler and its own express.raw():

app.post(
  '/webhooks/stripe',
  express.raw({ type: 'application/json' }),
  toExpressHandler(stripeWebhook),
);

app.post(
  '/webhooks/github',
  express.raw({ type: 'application/json' }),
  toExpressHandler(githubWebhook),
);

Response behavior

toExpressHandler writes the response itself — it calls res.status(...).json(...) (or .send() for a 204) based on the pipeline's outcome and never calls next(). Don't add response logic after it in the middleware chain; that code won't run.

If verification fails, the handler responds with the status code documented on the matching error page (e.g. 401 for invalid_signature) before onEvent is ever invoked.

Fast-acking long-running work

toExpressHandler waits for onEvent to resolve before responding. If your handler does slow work (external API calls, heavy DB writes), either keep it fast or hand off to a queue — see the BullMQ fast-ack pattern in the idempotency guide.

TypeScript config

No special tsconfig.json settings are required beyond strict: true. toExpressHandler's generic parameter is inferred from the handler you pass it, so req/event types flow through without manual annotation.

Last updated on

On this page