Verifying Lemon Squeezy Webhook Signatures
When a purchase is completed, Lemon Squeezy fires a webhook notification. To prevent malicious actors from triggering premium upgrades, verify the request cryptographic signature.
1. Webhook Signature Verification Architecture
Lemon Squeezy signs each request using a hash-based message authentication code (HMAC) with the SHA-256 hashing algorithm.
The signature is sent inside the X-Signature header of the incoming POST request. To verify it:
- Recreate the hash using your local Webhook Secret and the raw payload body.
- Compare your hash with the X-Signature header value using a secure timing-safe compare operation.
2. Implementing the Next.js Webhook Verifier
Write the middleware verifier inside a Route Handler:
// app/api/webhooks/lemonsqueezy/route.ts
import { NextResponse } from "next/server";
import crypto from "crypto";
export async function POST(request: Request) {
const signature = request.headers.get("x-signature");
const rawRequestBody = await request.text();
if (!signature) {
return new Response("Missing signature header", { status: 400 });
}
const secret = process.env.LEMONSQUEEZY_WEBHOOK_SECRET;
if (!secret) {
return new Response("Webhook secret not configured on server", { status: 500 });
}
// 1. Calculate HMAC-SHA256 hash using local secret key
const calculatedSignature = crypto
.createHmac("sha256", secret)
.update(rawRequestBody)
.digest("hex");
// 2. Perform timing-safe comparison to prevent timing attacks
const isMatch = crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(calculatedSignature, "hex")
);
if (!isMatch) {
console.warn("Invalid webhook signature.");
return new Response("Signature mismatch", { status: 401 });
}
// 3. Process the verified event payload
const payload = JSON.parse(rawRequestBody);
const eventName = payload.meta.event_name; // e.g. "order_created"
console.log("Verified Lemon Squeezy event:", eventName);
return NextResponse.json({ received: true });
}Published on Last updated: