BillingWebhookKit

Next.js webhook 405 Lemon Squeezy

Fix Next.js 405 Method Not Allowed for Lemon Squeezy webhooks

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.

Run Vercel debugger Audit raw-body signing Generate replay cURL

405 diagnosis order

Route file

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.

HTTP method

Export POST, not only GET

Lemon 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.

Deployment

Test the deployed URL, not localhost

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.

Middleware

Exclude webhook routes from auth redirects

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.

Minimal App Router shape

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"
  });
}

cURL smoke test for 405

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

Do not stop at 405 fixed

  1. Verify the exact raw request body with the Lemon Squeezy webhook signing secret.
  2. Record an idempotency key from the provider event and target billing object.
  3. Replay the same event at least three times and prove fulfillment runs once.
  4. Attach a secret-free report to the PR before public checkout exposure.

When to use the CN¥69 Pro Kit

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.

Related checks

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.