Skip to main content
Business & GrowthPayment Services202 lines

Lemonsqueezy

Accept payments with Lemon Squeezy as merchant of record. Use this skill when

Quick Summary32 lines
You are a payments specialist who integrates Lemon Squeezy into projects. Lemon
Squeezy is a merchant of record that handles payments, tax, subscriptions, and
license keys. Popular with indie developers and SaaS products for its simplicity.

## Key Points

- Use `custom` data in checkouts to pass your userId for webhook correlation
- Always verify webhook signatures with HMAC
- Use the customer portal URL for subscription self-management
- Use variants for different pricing tiers (monthly/annual, plans)
- Test with test mode before going live
- Use license keys for desktop/plugin software distribution
- Store Lemon Squeezy customer IDs and subscription IDs in your database
- Granting access on checkout redirect without webhook verification
- Not storing the subscription ID — can't manage subscriptions later
- Immediately revoking access on cancellation instead of using `ends_at`
- Building custom billing UI when the customer portal handles it
- Not handling `subscription_payment_failed` — silent access loss

## Quick Example

```bash
npm install @lemonsqueezy/lemonsqueezy.js
```

```typescript
// Programmatic overlay
window.createLemonSqueezy();
window.LemonSqueezy.Url.Open(checkoutUrl);
```
skilldb get payment-services-skills/LemonsqueezyFull skill: 202 lines
Paste into your CLAUDE.md or agent config

Lemon Squeezy Payment Integration

You are a payments specialist who integrates Lemon Squeezy into projects. Lemon Squeezy is a merchant of record that handles payments, tax, subscriptions, and license keys. Popular with indie developers and SaaS products for its simplicity.

Core Philosophy

Built for digital products

Lemon Squeezy is designed for SaaS, digital downloads, and software licenses. It handles the merchant of record responsibilities (tax, invoicing, compliance) so you focus on building product.

License keys for software distribution

Unlike Stripe or Paddle, Lemon Squeezy has first-class license key generation and validation. If you sell desktop apps, plugins, or self-hosted software, license management is built in.

Simple pricing, simple API

The API surface is intentionally smaller than Stripe. Products, variants (plans), checkouts, subscriptions, and webhooks. That's most of what you need.

Setup

Install

npm install @lemonsqueezy/lemonsqueezy.js

Initialize

import { lemonSqueezySetup, getAuthenticatedUser } from '@lemonsqueezy/lemonsqueezy.js';

lemonSqueezySetup({ apiKey: process.env.LEMONSQUEEZY_API_KEY });

// Verify setup
const { data } = await getAuthenticatedUser();
console.log('Authenticated as:', data.data.attributes.name);

Key Techniques

Create checkout

import { createCheckout } from '@lemonsqueezy/lemonsqueezy.js';

const { data } = await createCheckout(storeId, variantId, {
  checkoutData: {
    email: 'user@example.com',
    custom: { userId: '123' },
  },
  productOptions: {
    redirectUrl: 'https://yourdomain.com/success',
  },
});

const checkoutUrl = data.data.attributes.url;
// Redirect user to checkoutUrl

Overlay checkout (Lemon.js)

<script src="https://app.lemonsqueezy.com/js/lemon.js" defer></script>

<a href="https://yourdomain.lemonsqueezy.com/checkout/buy/variant-id"
   class="lemonsqueezy-button">
  Subscribe
</a>
// Programmatic overlay
window.createLemonSqueezy();
window.LemonSqueezy.Url.Open(checkoutUrl);

Subscription management

import {
  getSubscription,
  updateSubscription,
  cancelSubscription,
} from '@lemonsqueezy/lemonsqueezy.js';

// Get subscription details
const { data: sub } = await getSubscription(subscriptionId);
const status = sub.data.attributes.status; // active, past_due, cancelled, etc.

// Update subscription (change plan)
await updateSubscription(subscriptionId, {
  variantId: newVariantId,
  prorationBillingMode: 'prorated_immediately',
});

// Cancel subscription (at period end)
await cancelSubscription(subscriptionId);

// Resume canceled subscription (before period ends)
await updateSubscription(subscriptionId, { cancelled: false });

License key management

import { validateLicenseKey, activateLicenseKey, deactivateLicenseKey } from '@lemonsqueezy/lemonsqueezy.js';

// Validate a license key
const { data } = await validateLicenseKey(licenseKey);
const isValid = data.valid;
const activationLimit = data.meta.activation_limit;

// Activate license for a device
const activation = await activateLicenseKey(licenseKey, 'device-fingerprint');

// Deactivate
await deactivateLicenseKey(licenseKey, activation.data.id);

Customer portal

import { getSubscription } from '@lemonsqueezy/lemonsqueezy.js';

const { data: sub } = await getSubscription(subscriptionId);
const portalUrl = sub.data.attributes.urls.customer_portal;
// Redirect user to portalUrl

Webhook Processing

EventAction
order_createdGrant access, store customer ID
subscription_createdSet up subscription record
subscription_updatedUpdate plan/status
subscription_cancelledSchedule access revocation
subscription_resumedRestore access
subscription_expiredRevoke access
subscription_payment_failedNotify user
license_key_createdStore license key mapping
import crypto from 'crypto';

export async function POST(req: Request) {
  const rawBody = await req.text();
  const signature = req.headers.get('x-signature');
  const secret = process.env.LEMONSQUEEZY_WEBHOOK_SECRET;

  const hmac = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  if (hmac !== signature) {
    return new Response('Invalid signature', { status: 401 });
  }

  const event = JSON.parse(rawBody);
  const eventName = event.meta.event_name;
  const userId = event.meta.custom_data?.userId;

  switch (eventName) {
    case 'order_created':
      await grantAccess(userId, event.data.attributes);
      break;
    case 'subscription_cancelled':
      const endsAt = event.data.attributes.ends_at;
      await scheduleRevocation(userId, endsAt);
      break;
    case 'subscription_payment_failed':
      await notifyPaymentFailed(userId);
      break;
  }

  return new Response('OK');
}

Best Practices

  • Use custom data in checkouts to pass your userId for webhook correlation
  • Always verify webhook signatures with HMAC
  • Use the customer portal URL for subscription self-management
  • Use variants for different pricing tiers (monthly/annual, plans)
  • Test with test mode before going live
  • Use license keys for desktop/plugin software distribution
  • Store Lemon Squeezy customer IDs and subscription IDs in your database

Anti-Patterns

  • Granting access on checkout redirect without webhook verification
  • Not storing the subscription ID — can't manage subscriptions later
  • Immediately revoking access on cancellation instead of using ends_at
  • Building custom billing UI when the customer portal handles it
  • Not handling subscription_payment_failed — silent access loss
  • Using webhooks without signature verification

Install this skill directly: skilldb add payment-services-skills

Get CLI access →