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_SECRET set

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}'