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-Signature header?
  • Why is my Lemon Squeezy webhook signature verification failing?
  • How do I handle order_created, subscription_created, or license_key_created events?
  • 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.

EventDescription
order_createdNew order placed (also sent alongside subscription_created and license_key_created)
order_refundedFull or partial refund on an order
subscription_createdNew subscription created
subscription_updatedAny subscription change; works as a catch-all
subscription_cancelledCancelled; stays active until the end of the grace period
subscription_payment_successSubscription payment succeeded (payload is a subscription invoice)
license_key_createdLicense key issued from a new order

Also available: customer_updated, subscription_resumed, subscription_expired, subscription_paused, subscription_unpaused, subscription_payment_failed, subscription_payment_recovered, subscription_payment_refunded, license_key_updated, affiliate_activated.

For the full event reference, see Lemon Squeezy event types

Important Headers

HeaderDescription
X-SignatureHex HMAC-SHA256 of the raw body
X-Event-NameEvent type, e.g. order_created
Content-Typeapplication/json (JSON:API resource object)

Gotchas

  • Payloads are JSON:API: the resource is data (data.id, data.attributes), and your checkout custom_data is in meta.custom_data.
  • Respond 200 to 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_payment_success), and ordering is not guaranteed.

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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 2, 2026

View on GitHub →