Confirm App Router file location
The route should live at a path such as app/api/webhooks/lemonsqueezy/route.ts. A page file, server action, or Pages Router path mismatch can make the deployed URL behave differently than expected.
Next.js webhook 405 Lemon Squeezy
A 405 means the deployed route exists, but it is not accepting the HTTP method Lemon Squeezy is sending. For webhook launch work, fix this before signature debugging, fulfillment, or public checkout exposure.
The route should live at a path such as app/api/webhooks/lemonsqueezy/route.ts. A page file, server action, or Pages Router path mismatch can make the deployed URL behave differently than expected.
POST, not only GETLemon Squeezy sends webhook events with POST. If your route only exports GET, or wraps the handler in a helper that only handles GET, Next.js can return 405.
Check the exact Vercel production endpoint configured in Lemon Squeezy. A preview deployment, stale tunnel, missing base path, or trailing segment mismatch can hide the real failing route.
Auth middleware, bot filters, rewrites, and redirect rules can convert a webhook POST into a blocked request. The route must accept unsigned network traffic long enough to perform its own HMAC verification.
Start with a small route that proves POST delivery and raw body access. Add idempotency and fulfillment only after this returns 2xx for a signed fixture.
// app/api/webhooks/lemonsqueezy/route.ts
export const runtime = "nodejs";
export async function POST(request: Request) {
const rawBody = await request.text();
const signature = request.headers.get("x-signature");
if (!signature) {
return new Response("Missing Lemon Squeezy x-signature", { status: 400 });
}
// Verify HMAC with your webhook signing secret before JSON.parse(rawBody).
// Keep API keys, signing secrets, full customer payloads, and live order data out of logs.
const event = JSON.parse(rawBody);
return Response.json({
ok: true,
eventName: event?.meta?.event_name ?? "unknown"
});
}
Before opening a public checkout link, send a POST to the deployed webhook URL. A 405 here proves the method gate is still broken. A 2xx only proves route acceptance; signature, idempotency, and fulfillment still need separate checks.
curl -i -X POST "https://your-app.vercel.app/api/webhooks/lemonsqueezy" \
-H "content-type: application/json" \
-H "x-signature: fake_signature_for_route_smoke_test" \
--data '{"meta":{"event_name":"order_created"},"data":{"id":"order_fake_405"}}'
Use the free browser tools first if you only need to find the 405. The Pro Kit is positioned for teams that want copy-ready fixtures, route handlers, raw-body tests, contract tests, duplicate replay checks, CI workflow, and a webhook review report template.
Safety boundary: use fake payloads and fake signing secrets in public reports. Never paste Lemon Squeezy API keys, webhook signing secrets, private checkout links, customer data, or full live order payloads into shared debugging artifacts.