Webhook monitoring for Next.js.
Drop-in observability for App Router and Pages Router webhook handlers. Wraps your existing route — never modifies the request or response. Less than 3ms of overhead per webhook.
Add the package
npm install @outworx/hooks
# or
pnpm add @outworx/hooks
# or
yarn add @outworx/hooksSet environment variables
Grab your API key from the dashboard and add it to .env.local. Set the webhook secret for whichever provider you're wiring up too.
# .env.local
OUTWORX_HOOKS_API_KEY=ow_live_...
STRIPE_WEBHOOK_SECRET=whsec_...App Router
For routes under app/. The wrapper exports a POST handler — signature is verified before your code runs, and duplicate retries (same event ID within 24h) short-circuit with the cached response.
// app/api/webhooks/stripe/route.ts
import { init } from '@outworx/hooks';
import { withWebhookMonitoring } from '@outworx/hooks/nextjs';
init({ apiKey: process.env.OUTWORX_HOOKS_API_KEY! });
export const POST = withWebhookMonitoring(
{
provider: 'stripe',
signatureSecret: process.env.STRIPE_WEBHOOK_SECRET!,
idempotencyKey: (_req, body) => (body as any).id,
},
async (req) => {
const body = await req.json();
// Signature verified, duplicates filtered.
return Response.json({ received: true });
}
);Pages Router
For routes under pages/api/. Disable Next's body parser so the raw bytes reach the signature verifier.
// pages/api/webhooks/stripe.ts
import { init } from '@outworx/hooks';
import { withWebhookMonitoring } from '@outworx/hooks/nextjs/pages';
init({ apiKey: process.env.OUTWORX_HOOKS_API_KEY! });
export const config = {
api: { bodyParser: false }, // raw body needed for signature
};
export default withWebhookMonitoring(
{
provider: 'stripe',
signatureSecret: process.env.STRIPE_WEBHOOK_SECRET!,
},
async (req, res) => {
res.status(200).json({ received: true });
}
);GitHub, Shopify, Clerk, and any custom provider
The same wrapper works for every provider. Built-in HMAC-SHA256 verification covers Stripe, GitHub, Shopify, Svix / Clerk, and Slack. For anything else, pass a signatureVerifier function and you're good.
// app/api/webhooks/[provider]/route.ts — one route, many providers
import { init } from '@outworx/hooks';
import { withWebhookMonitoring } from '@outworx/hooks/nextjs';
init({ apiKey: process.env.OUTWORX_HOOKS_API_KEY! });
export const POST = withWebhookMonitoring(
{
provider: 'github',
signatureSecret: process.env.GITHUB_WEBHOOK_SECRET!,
idempotencyKey: (_req, _body, headers) => headers['x-github-delivery'],
},
async (req) => {
// ... handle GitHub event
return Response.json({ ok: true });
}
);Catch silent drops
A handler that returns 200 but never actually processes the event looks identical to a real success on every webhook tool — except ours. Set requireProcessingMark and call track.processed() when your business logic actually finishes:
export const POST = withWebhookMonitoring(
{
provider: 'stripe',
signatureSecret: process.env.STRIPE_WEBHOOK_SECRET!,
requireProcessingMark: true, // ← opt in
},
async (req, { track }) => {
const event = await stripe.webhooks.constructEventAsync(...);
await chargeCustomer(event);
track.processed(); // ← explicit ack
return Response.json({ received: true });
}
);Full guide: Silent drop detection.
Configuration reference
Full option list — including captureBody, metadata, idempotencyTtl, and the standalone signature verifiers — lives in the main SDK reference.