| name | lemon-squeezy-integration |
| description | Full Lemon Squeezy integration — REST API, @lemonsqueezy/lemonsqueezy.js server SDK, Lemon.js checkout overlays, webhooks, custom_data, and subscriptions. Use when integrating Lemon Squeezy billing, checkout, webhooks, LS API, Lemon.js, or subscription provisioning into an application. |
Lemon Squeezy Integration
Official docs: https://docs.lemonsqueezy.com/
Architecture
Three layers — use all three for a production SaaS integration:
| Layer | Tool | Runs on |
|---|
| Checkout UX | Lemon.js + checkout URLs | Browser |
| Admin / dynamic checkout | REST API or @lemonsqueezy/lemonsqueezy.js | Server only |
| Provisioning | Webhooks | Server endpoint |
Never expose LEMONSQUEEZY_API_KEY in client code. The npm SDK is server-only.
Golden rule: Webhooks are the source of truth for granting access. Lemon.js Checkout.Success is UX-only — do not provision solely from client events.
Choose an approach
| Need | Use |
|---|
| Static pricing page | Dashboard checkout URLs + optional Lemon.js overlay |
| Per-user checkout with custom data | Append checkout[custom][user_id] to URL, or create checkout via API |
| Dynamic pricing / on-demand checkout | Server: createCheckout → open URL with Lemon.js |
| Grant/revoke access after payment | Webhook handler |
| Customer billing portal | subscription.urls.update_payment_method from API + Lemon.js overlay |
Required environment variables
| Variable | Purpose |
|---|
LEMONSQUEEZY_API_KEY | Server API auth (Settings → API) |
LEMONSQUEEZY_STORE_ID | Store ID for checkout creation |
LEMONSQUEEZY_WEBHOOK_SECRET | Verify incoming webhook X-Signature |
Use test mode keys until production. Docs: https://docs.lemonsqueezy.com/help/getting-started/test-mode
End-to-end workflow
1. Store setup
Create products and variants in the LS dashboard (or sync via API). Record store_id, product_id, and variant_id for each plan.
2. Checkout
Static URL:
https://[STORE].lemonsqueezy.com/checkout/buy/[VARIANT_ID]
With user linking (required for webhook provisioning):
?checkout[custom][user_id]=USER_ID&checkout[email]=user@example.com
Overlay: Load Lemon.js, add lemonsqueezy-button class to links, or call LemonSqueezy.Url.Open(url).
Dynamic: Server creates checkout via API/SDK → use data.attributes.url from response.
Share /checkout/buy/ URLs only. Do not share converted /checkout/?cart= URLs — they are single-use per customer.
PocketHost uses custom domain URLs like https://store.pockethost.io/buy/... — see pockethost.md.
3. Webhook endpoint
- Register URL in Settings → Webhooks (or via API)
- Verify HMAC-SHA256 of the raw request body against
X-Signature header
- Read
meta.event_name and meta.custom_data
- Update your database idempotently
- Return 200 quickly
Key events: order_created, subscription_created, subscription_updated, subscription_expired, subscription_cancelled, order_refunded, subscription_payment_refunded
4. Persist subscription state
Store at minimum: LS subscription/order id, status, variant_id, renews_at, ends_at, and your internal plan tier mapping.
On refund/expiry/cancel → revoke or decrement access.
Common tasks
Add a new plan
- Create variant in LS dashboard
- Add checkout URL to frontend (with
checkout[custom][user_id])
- Add
{product_id}-{variant_id} to webhook allowlist and product handler map
- Map variant → internal subscription fields
Debug a failed webhook
- Confirm webhook URL and signing secret match env
- Check LS dashboard webhook log for payload and response
- Verify signature uses raw body (not re-serialized JSON)
- Confirm
meta.custom_data.user_id was passed at checkout
- Confirm product/variant ID is in your handler allowlist
Reference files