Fastify
Integrate hooksentinel with Fastify using a raw content-type parser and the built-in Fastify adapter.
Install
npm install @hooksentinel/coretoFastifyHandler is exported from @hooksentinel/core/fastify.
The raw body requirement
Fastify parses application/json bodies by default, which destroys the raw bytes hooksentinel needs to verify signatures. Register a raw content-type parser scoped to your webhook route(s) so the rest of your app keeps normal JSON parsing.
import Fastify from 'fastify';
import { toFastifyHandler } from '@hooksentinel/core/fastify';
import { createWebhookHandler, stripe } from '@hooksentinel/core';
import { memoryStore } from '@hooksentinel/core/stores';
const app = Fastify();
app.addContentTypeParser(
'application/json',
{ parseAs: 'buffer' },
(_req, body, done) => done(null, body),
);
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', toFastifyHandler(stripeWebhook));
async function fulfillOrder(sessionId: string) {
// ...
}
app.listen({ port: 3000 });Overriding the global content-type parser to always return a buffer means every route on this Fastify instance now receives a raw Buffer for application/json bodies, not just the webhook route. If you have other JSON routes on the same instance, register hooksentinel's routes on a scoped plugin instead so the override doesn't leak:
import type { FastifyPluginAsync } from 'fastify';
import { toFastifyHandler } from '@hooksentinel/core/fastify';
import { stripeWebhook } from './stripe';
export const webhookRoutes: FastifyPluginAsync = async (instance) => {
instance.addContentTypeParser(
'application/json',
{ parseAs: 'buffer' },
(_req, body, done) => done(null, body),
);
instance.post('/webhooks/stripe', toFastifyHandler(stripeWebhook));
};import Fastify from 'fastify';
import { webhookRoutes } from './webhooks/plugin';
const app = Fastify();
await app.register(webhookRoutes, { prefix: '/webhooks' });
app.listen({ port: 3000 });Because the raw parser is registered on the scoped plugin instance, routes outside it keep Fastify's normal JSON parsing.
Response behavior
toFastifyHandler returns a Fastify route handler function — pass it directly as the handler argument to app.post(), app.route(), etc. It calls reply.code(...).send(...) internally based on the pipeline outcome; don't write to reply after calling into it.
Multiple providers
Register one route per provider inside the scoped plugin, each pointing at its own createWebhookHandler instance:
instance.post('/stripe', toFastifyHandler(stripeWebhook));
instance.post('/github', toFastifyHandler(githubWebhook));Last updated on