Agent skill
Lemon Squeezy Webhooks Skill
Receive and verify Lemon Squeezy webhooks. Use when setting up Lemon Squeezy webhook handlers, debugging X-Signature verification failures, or handling order_created, subscription_created, subscription_payment_success, or license_key_created events.
Install this skill
npx skills add hookdeck/webhook-skills --skill lemon-squeezy-webhooks
When to Use This Skill
- How do I receive Lemon Squeezy webhooks?
- How do I verify the Lemon Squeezy
X-Signatureheader? - Why is my Lemon Squeezy webhook signature verification failing?
- How do I handle
order_created,subscription_created, orlicense_key_createdevents? - How do I get my own user ID from a Lemon Squeezy webhook (
custom_data)?
Verification (core)
Lemon Squeezy sends X-Signature: the lowercase hex HMAC-SHA256 of the raw request body, keyed with the signing secret you entered when creating the webhook. There is no timestamp, no prefix and no SDK helper. The secret is a plain 6-40 character string, used as-is (not base64-decoded).
Node:
const crypto = require('crypto');
function verify(rawBody, signatureHeader, secret) {
const expected = Buffer.from(crypto.createHmac('sha256', secret).update(rawBody).digest('hex'), 'utf8');
const received = Buffer.from(signatureHeader || '', 'utf8');
return expected.length === received.length && crypto.timingSafeEqual(expected, received);
}
Python:
import hmac, hashlib
def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected.encode(), (signature_header or "").encode())
For complete handlers with route wiring, event dispatch, and tests, see:
Common Event Types
The event name is in the X-Event-Name header and in meta.event_name in the body.
| Event | Description |
|---|---|
order_created | New order placed (also sent alongside subscription_created and license_key_created) |
order_refunded | Full or partial refund on an order |
subscription_created | New subscription created |
subscription_updated | Any subscription change; works as a catch-all |
subscription_cancelled | Cancelled; stays active until the end of the grace period |
subscription_ | Subscription payment succeeded (payload is a subscription invoice) |
license_key_created | License key issued from a new order |
Also available: customer_updated, subscription_resumed, subscription_expired, subscription_paused, subscription_unpaused, subscription_, subscription_, subscription_, license_key_updated, affiliate_activated.
For the full event reference, see Lemon Squeezy event types
Important Headers
| Header | Description |
|---|---|
X-Signature | Hex HMAC-SHA256 of the raw body |
X-Event-Name | Event type, e.g. order_created |
Content-Type | application/json (JSON:API resource object) |
Gotchas
- Payloads are JSON:API: the resource is
data(data.id,data.attributes), and your checkoutcustom_datais inmeta.custom_data. - Respond
200to acknowledge. Any other status is retried up to 3 more times with exponential backoff (about 5s, 25s, 125s). - There is no delivery ID or timestamp header, so make handlers idempotent (for example key on event name +
data.id+data.attributes.updated_).at - The same purchase fires several events (a subscription sale sends
order_created,subscription_created,subscription_), and ordering is not guaranteed.payment_ success
Environment Variables
LEMON_SQUEEZY_SIGNING_SECRET=your_signing_secret # Entered when creating the webhook (6-40 chars)
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 lemon-squeezy --path /webhooks/lemon-squeezy
Reference Materials
- references/overview.md - Lemon Squeezy webhook concepts and events
- references/setup.md - Dashboard configuration guide
- references/verification.md - Signature verification details