Lemon Squeezy webhook says the signature is invalid
Lemon Squeezy signs the raw request body with HMAC SHA-256 and sends the result in the X-Signature header. If you hash anything other than the exact raw text, the check fails.
What you might see
- Your code returns 400 or 401 for every Lemon Squeezy event.
- The same code passes in your own tests.
- Lemon Squeezy shows failed deliveries.
Why it happens
The signing secret is the one you typed when you created the webhook. It is not your API key.
The signature is a hex string made from the raw body. If your framework parses the JSON and you stringify it again, the bytes can differ and the hash will not match.
How to fix it
- Hash the raw body. Read the body as text. Hash that text with your signing secret. Compare it to the X-Signature header.
import crypto from "node:crypto"; const raw = await req.text(); const sig = req.headers.get("x-signature") ?? ""; const hmac = crypto.createHmac("sha256", secret).update(raw).digest("hex"); const ok = sig.length === hmac.length && crypto.timingSafeEqual(Buffer.from(hmac), Buffer.from(sig)); - Check the secret. Use the signing secret from the webhook settings. Look for spaces or a line break at the end of the value in your environment variables.
- Compare in constant time. Use timingSafeEqual, and check the lengths first. It throws if the lengths differ.
- Check test and live mode. Test mode and live mode have separate webhooks. Make sure each one uses its own secret.
- Skip repeats. Lemon Squeezy can send the same event again. Store what you handled and ignore repeats.
Check your fix
Scan your webhook address with Tillping and choose Lemon Squeezy. We send a fake X-Signature. Your endpoint should reject it. If it accepts it, fix that first.
Run the free check
Paste your webhook address and see what your endpoint answers. No account. No keys.
Check my webhook