Recipe
Webhook handler with signature verification
Receive and validate signed webhooks from SellAuth, Stripe, or any HMAC-SHA256 provider.
Overview
This handler verifies the X-Signature header against the raw request body using a shared secret. If the signature mismatches, it returns 401 immediately — no processing occurs.
Prerequisites
- Next.js 14 App Router with Route Handlers
- A webhook secret from your provider
- Environment variable
WEBHOOK_SECRETset
Route handler
// app/api/webhook/route.ts
import { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";
export async function POST(req: NextRequest) {
const raw = await req.text();
const sig = req.headers.get("x-signature");
const expected = crypto
.createHmac("sha256", process.env.WEBHOOK_SECRET!)
.update(raw)
.digest("hex");
if (!sig || sig !== expected) {
return NextResponse.json(
{ error: "invalid signature" },
{ status: 401 }
);
}
const payload = JSON.parse(raw);
// Process event here
return NextResponse.json({ received: true });
}Timing-safe comparison
For production, use crypto.timingSafeEqual to prevent timing attacks:
const sigBuf = Buffer.from(sig, "hex");
const expBuf = Buffer.from(expected, "hex");
if (sigBuf.length !== expBuf.length ||
!crypto.timingSafeEqual(sigBuf, expBuf)) {
return NextResponse.json(
{ error: "invalid signature" },
{ status: 401 }
);
}Testing locally
Use the Stripe CLI or a simple curl to test:
curl -X POST http://localhost:3000/api/webhook \
-H "Content-Type: application/json" \
-H "X-Signature: $(echo -n '{"test":true}' | \
openssl dgst -sha256 -hmac "your-secret" | cut -d' ' -f2)" \
-d '{"test":true}'