Test Lemon Squeezy webhooks without deploying
Capture order and subscription events from Lemon Squeezy, and verify the hex X-Signature header.
Get a URL that accepts the webhook#
A capture URL takes any method, any body, and records exactly what arrived: method, headers, query string, body and source IP. You do not need an account to make one, and an anonymous URL is deleted after 24 hours.
curl -X POST https://mocklane.com/api/v1/anon/endpoints \
-H "Content-Type: application/json" -d '{}'
# The response carries a capture URL of the form:
# https://mocklane.com/h/<endpoint-id>Paste that URL into Lemon Squeezy at Lemon Squeezy dashboard, Settings, Webhooks, then trigger something. Events worth starting with:
order_createdsubscription_createdsubscription_payment_successsubscription_cancelled
Verify the X-Signature header#
Lemon Squeezy signs each request so you can tell a real delivery from anyone who has guessed your URL. The digest is HMAC-SHA256 over the raw body, hex-encoded, keyed with your signing secret, set per webhook.
- Header
X-Signature- Algorithm
- HMAC-SHA256, hex
- Signed content
- the raw body
const crypto = require("crypto");
// The simplest scheme on this list: hex HMAC-SHA256 over the raw body.
const expected = crypto
.createHmac("sha256", process.env.LEMONSQUEEZY_WEBHOOK_SECRET)
.update(rawBody)
.digest("hex");
const ok = crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(req.headers["x-signature"]),
);crypto.timingSafeEqual. A plain === returns as soon as two bytes differ, and that timing difference is enough to recover a valid signature one byte at a time.What catches people out#
- The header is the bare hex digest with no prefix and no timestamp, so there is no replay protection built into the scheme. Deduplicate on the event id yourself.
- The secret is set per webhook, so each endpoint you add has its own.
- Verification is over the raw body, so capture it before any JSON middleware.
Replay it until your handler is right#
A captured request can be re-sent to your own endpoint, with the body or headers edited first. That turns a bug that needed a real Lemon Squeezy event into something you can reproduce on demand, which matters most for events that are awkward to trigger twice.
Assert on it in CI#
A scenario chains the steps into one run that passes or fails, so a change that breaks your Lemon Squeezy handling fails the build rather than reaching production.
steps:
- type: send_webhook
body: { event: "order_created" }
- type: wait_for_webhook
timeout_seconds: 30
assert:
- status_code equals 200Triggered from CI with an API token. See the scenarios documentation for the full step list.
Signature details checked against Lemon Squeezy’s documentation on 22 October 2026. Schemes change: if this page and their docs disagree, believe theirs and tell us.