# Lemon Squeezy Integration

> 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.

- Skill: `pockethost/lemon-squeezy-integration` (Agent Skill, multi-file: 7 files)
- Install (CLI): `npx skillmds@latest add pockethost/lemon-squeezy-integration`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pockethost/lemon-squeezy-integration/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: pockethost (https://skillmd.com/u/pockethost)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/pockethost/lemon-squeezy-integration

---


# 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](https://app.lemonsqueezy.com/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](pockethost.md).

### 3. Webhook endpoint

1. Register URL in Settings → Webhooks (or via API)
2. Verify HMAC-SHA256 of the **raw request body** against `X-Signature` header
3. Read `meta.event_name` and `meta.custom_data`
4. Update your database idempotently
5. 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

1. Create variant in LS dashboard
2. Add checkout URL to frontend (with `checkout[custom][user_id]`)
3. Add `{product_id}-{variant_id}` to webhook allowlist and product handler map
4. Map variant → internal subscription fields

### Debug a failed webhook

1. Confirm webhook URL and signing secret match env
2. Check LS dashboard webhook log for payload and response
3. Verify signature uses raw body (not re-serialized JSON)
4. Confirm `meta.custom_data.user_id` was passed at checkout
5. Confirm product/variant ID is in your handler allowlist

## Reference files

- [api.md](api.md) — REST API essentials
- [server-sdk.md](server-sdk.md) — `@lemonsqueezy/lemonsqueezy.js`
- [lemonjs.md](lemonjs.md) — browser checkout library
- [webhooks.md](webhooks.md) — verification, events, payload shape
- [integration-checklist.md](integration-checklist.md) — ship checklist
- [pockethost.md](pockethost.md) — PocketHost-specific wiring (this repo)

