Back to roadmaps lemonsqueezy Course

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:

  1. Recreate the hash using your local Webhook Secret and the raw payload body.
  2. 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: