Missing env vars or malformed headers
Confirm the Vercel production deployment has the webhook signing secret, store or variant IDs, and any database or delivery credentials required by the handler. Log only boolean presence, never secret values.
Lemon Squeezy webhook 500 Vercel
A 500 means Lemon Squeezy reached your endpoint and the handler crashed or rejected the event after route matching. Fix this before exposing checkout, because retries can duplicate emails, licenses, downloads, and access grants.
Confirm the Vercel production deployment has the webhook signing secret, store or variant IDs, and any database or delivery credentials required by the handler. Log only boolean presence, never secret values.
Read request.text() once, compare equal-length HMAC buffers, and reject mismatches with 400. A length mismatch passed into timingSafeEqual can throw and become a 500.
Validate meta.event_name, object ID, payment status, product or variant, and customer target before fulfillment. Unknown events should quarantine cleanly, not crash the route.
Record idempotency before running side effects where possible. If a retry happens, the route must skip duplicate delivery instead of sending another download, email, or license key.
Separate unauthenticated failures, unsupported events, and retryable fulfillment failures. Do not let every exception become an uncontrolled 500.
export async function POST(request: Request) {
const rawBody = await request.text();
const signature = request.headers.get("x-signature");
if (!signature) return new Response("Missing x-signature", { status: 400 });
if (!process.env.LEMONSQUEEZY_WEBHOOK_SECRET) {
return new Response("Webhook secret not configured", { status: 500 });
}
const verified = await verifyLemonSignature(rawBody, signature);
if (!verified) return new Response("Invalid signature", { status: 400 });
const event = JSON.parse(rawBody);
const idempotencyKey = `lemonsqueezy:${event.meta?.event_name}:${event.data?.id}`;
try {
await processOnce(idempotencyKey, async () => {
await runPaidOrderFulfillment(event);
});
return Response.json({ ok: true });
} catch {
return new Response("Retryable fulfillment failure", { status: 500 });
}
}
Returning 500 can be correct when a verified paid event cannot be durably processed and Lemon Squeezy should retry. Returning 500 is not correct for bad signatures, unsupported events, wrong products, missing customer targets, or duplicate deliveries that have already been processed.
Safety boundary: use fake payloads and redacted logs in public reports. Never paste Lemon Squeezy API keys, webhook signing secrets, private checkout links, customer data, database URLs, or full live order payloads into shared debugging artifacts.