Next.js App Router + Stripe webhook handler: working versions (tested 2026-10-15)
A Next.js route handler that verifies Stripe webhook signatures from the raw body and handles checkout.session.completed.
passed3 passed · 0 failed2026-10-15 11:36 UTC- Status
- passed
- Last passed
- 2026-10-15 11:36 UTC
- Tests
- 3 / 3
- Language
- TypeScript
- Runtime
- node v22.23.3
npm install --no-audit --no-fund --ignore-scripts=false npm test
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38728226240
The code
Exactly the files that passed. Your agent can pull them with get_recipe("nextjs-stripe-webhook") (install), or see them on GitHub.
{
"name": "nextjs-stripe-webhook",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {
"next": "16.4.1",
"stripe": "23.0.0",
"react": "19.3.1",
"react-dom": "19.3.1"
},
"devDependencies": {
"tsx": "4.23.15",
"@types/node": "24.19.2"
}
}
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? "sk_test_placeholder");
export const fulfilled: string[] = [];
export async function POST(req: Request): Promise<Response> {
const signature = req.headers.get("stripe-signature");
if (!signature) return Response.json({ error: "missing signature" }, { status: 400 });
const raw = await req.text(); // must be the raw body for signature checks
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(raw, signature, process.env.STRIPE_WEBHOOK_SECRET!);
} catch {
return Response.json({ error: "bad signature" }, { status: 400 });
}
if (event.type === "checkout.session.completed") {
const session = event.data.object as Stripe.Checkout.Session;
fulfilled.push(session.id);
}
return Response.json({ received: true });
}
import { test } from "node:test";
import assert from "node:assert/strict";
import Stripe from "stripe";
process.env.STRIPE_WEBHOOK_SECRET = "whsec_test_secret";
const { POST, fulfilled } = await import("../app/api/stripe/webhook/route.ts");
const stripe = new Stripe("sk_test_placeholder");
function signed(payload: object, secret = "whsec_test_secret") {
const body = JSON.stringify(payload);
const header = stripe.webhooks.generateTestHeaderString({ payload: body, secret });
return new Request("http://localhost/api/stripe/webhook", { method: "POST", body, headers: { "stripe-signature": header } });
}
const evt = { id: "evt_1", object: "event", type: "checkout.session.completed", data: { object: { id: "cs_test_123", object: "checkout.session" } } };
test("fulfills a correctly signed checkout event", async () => {
const res = await POST(signed(evt));
assert.equal(res.status, 200);
assert.ok(fulfilled.includes("cs_test_123"));
});
test("rejects a bad signature", async () => {
const res = await POST(signed(evt, "whsec_wrong"));
assert.equal(res.status, 400);
});
test("rejects a missing signature", async () => {
const res = await POST(new Request("http://localhost/x", { method: "POST", body: "{}" }));
assert.equal(res.status, 400);
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
Express 5 + Stripe webhook signature verification
Verify Stripe webhook signatures in Express 5 using the raw body, then dispatch on event type. Tested offline with generated test headers.
Next.js middleware + Supabase SSR session refresh
Refresh Supabase auth cookies in Next.js middleware with @supabase/ssr and redirect signed-out users away from protected routes.
Stripe Node SDK Checkout Sessions with a mocked HTTP client
Create Stripe Checkout Sessions with line items, metadata and idempotency keys, tested offline by plugging a fake fetch into Stripe.createFetchHttpClient.
FastAPI + Stripe webhook signature verification
Verify Stripe webhook signatures in FastAPI from the raw request body and dispatch on event type. Tested offline with locally signed payloads.