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-Signatureheader - Read
meta.event_nameandmeta.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_idwas passed at checkout - Confirm product/variant ID is in your handler allowlist
Reference files
- api.md — REST API essentials
- server-sdk.md —
@lemonsqueezy/lemonsqueezy.js - lemonjs.md — browser checkout library
- webhooks.md — verification, events, payload shape
- integration-checklist.md — ship checklist
- pockethost.md — PocketHost-specific wiring (this repo)