When you create a webhook, Lemon Squeezy asks you for a signing secret. It then sends an HMAC-SHA256 hex digest of the request body, keyed with that secret, in the X-Signature header. If your check fails, look at these first:
JSON.stringify(req.body). Middleware that parses JSON first is the most common cause..digest('hex'), not base64.crypto.timingSafeEqual throws if the buffers differ in length. Check the lengths first and treat a mismatch as invalid.app.post('/webhooks/lemonsqueezy', express.raw({ type: 'application/json' }), (req, res) => {
const digest = Buffer.from(crypto.createHmac('sha256', process.env.LS_SIGNING_SECRET).update(req.body).digest('hex'), 'utf8');
const sig = Buffer.from(req.get('X-Signature') || '', 'utf8');
if (sig.length !== digest.length || !crypto.timingSafeEqual(digest, sig)) return res.sendStatus(401);
const event = JSON.parse(req.body.toString('utf8')); // event name also in the X-Event-Name header
res.sendStatus(200);
});
export async function POST(req) {
const raw = await req.text();
const digest = crypto.createHmac('sha256', process.env.LS_SIGNING_SECRET).update(raw).digest('hex');
const sig = req.headers.get('x-signature') || '';
if (sig.length !== digest.length || !crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(sig)))
return new Response('invalid signature', { status: 401 });
// ...
return new Response('ok');
}
$payload = file_get_contents('php://input');
$hash = hash_hmac('sha256', $payload, getenv('LS_SIGNING_SECRET'));
if (!hash_equals($hash, $_SERVER['HTTP_X_SIGNATURE'] ?? '')) { http_response_code(401); exit; }
The guide is Stripe-only. The kit is a self-hosted Cloudflare Worker relay that adds retries, replay and a dead-letter list in front of your endpoint. It verifies Lemon Squeezy's hex X-Signature out of the box with its default scheme, and also supports Stripe, Shopify, Paddle Billing, Square, Twilio (status callbacks) and GitHub.
Fix Your Stripe Webhooks guide · Self-Hosted Webhook Relay Kit. Code LAUNCH40 = 40% off until Sun Oct 11, 6:40 AM MT.
Lemon Squeezy expects a 200. Its docs say that for any other status the webhook is retried up to three more times with exponential backoff, then marked failed. Respond quickly, and use the dashboard's webhook simulation to test without a real purchase.
Free download: Webhook debugging cheat sheet.
Fighting webhooks on more than one platform? The Fix Your Stripe Webhooks guide ($9) covers raw-body and signature problems in depth, and the Self-Hosted Webhook Relay Kit ($19) adds retries, replay and a dead-letter list in front of any endpoint.