| name | lemonsqueezy |
| description | Integrates payments and subscriptions with Lemon Squeezy merchant of record platform. Use when selling digital products, SaaS subscriptions, or software licenses with built-in tax handling. |
Lemon Squeezy
Merchant of record platform for digital products and SaaS. Handles payments, taxes, and compliance globally. Official JavaScript SDK for server-side integration.
Quick Start
npm install @lemonsqueezy/lemonsqueezy.js
Setup
import { lemonSqueezySetup, getAuthenticatedUser } from '@lemonsqueezy/lemonsqueezy.js';
lemonSqueezySetup({
apiKey: process.env.LEMONSQUEEZY_API_KEY,
onError: (error) => console.error('Lemon Squeezy Error:', error),
});
// Verify setup
const { data, error } = await getAuthenticatedUser();
if (error) {
console.error('Auth failed:', error.message);
} else {
console.log('Connected as:', data.attributes.name);
}
Create Checkout
Generate checkout URLs for customers to purchase.
import { createCheckout } from '@lemonsqueezy/lemonsqueezy.js';
async function createPaymentCheckout(variantId: string, customerEmail: string) {
const { data, error } = await createCheckout(process.env.LEMONSQUEEZY_STORE_ID!, {
productOptions: {
redirectUrl: 'https://myapp.com/success',
receiptButtonText: 'Go to Dashboard',
receiptThankYouNote: 'Thank you for your purchase!',
},
checkoutOptions: {
embed: true,
media: true,
logo: true,
},
checkoutData: {
email: customerEmail,
custom: {
userId: 'user_123',
},
},
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24 hours
preview: false,
}, {
relationships: {
variant: {
data: {
type: 'variants',
id: variantId,
},
},
},
});
if (error) throw new Error(error.message);
return data.attributes.url;
}
With Discount
const { data } = await createCheckout(storeId, {
checkoutData: {
discountCode: 'SAVE20',
email: customerEmail,
},
}, {
relationships: {
variant: {
data: { type: 'variants', id: variantId },
},
},
});
Products & Variants
import { listProducts, listVariants, getProduct, getVariant } from '@lemonsqueezy/lemonsqueezy.js';
// List all products
const { data: products } = await listProducts({
filter: { storeId: process.env.LEMONSQUEEZY_STORE_ID },
});
products.forEach((product) => {
console.log(product.attributes.name, product.attributes.price_formatted);
});
// Get single product
const { data: product } = await getProduct('prod_123');
// List variants for a product
const { data: variants } = await listVariants({
filter: { productId: 'prod_123' },
});
// Get variant details
const { data: variant } = await getVariant('var_123');
Subscriptions
import {
listSubscriptions,
getSubscription,
updateSubscription,
cancelSubscription,
} from '@lemonsqueezy/lemonsqueezy.js';
// List user's subscriptions
const { data: subscriptions } = await listSubscriptions({
filter: {
storeId: process.env.LEMONSQUEEZY_STORE_ID,
userEmail: 'customer@example.com',
},
});
// Get subscription details
const { data: subscription } = await getSubscription('sub_123');
console.log('Status:', subscription.attributes.status);
console.log('Renews at:', subscription.attributes.renews_at);
// Pause subscription
await updateSubscription('sub_123', {
pause: {
mode: 'void', // or 'free'
resumesAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
},
});
// Resume subscription
await updateSubscription('sub_123', {
pause: null,
});
// Change plan (upgrade/downgrade)
await updateSubscription('sub_123', {
variantId: 'new_variant_id',
invoiceImmediately: true,
});
// Cancel subscription
await cancelSubscription('sub_123');
License Keys
import {
listLicenseKeys,
getLicenseKey,
updateLicenseKey,
activateLicense,
validateLicense,
deactivateLicense,
} from '@lemonsqueezy/lemonsqueezy.js';
// List license keys
const { data: licenses } = await listLicenseKeys({
filter: { orderId: 'order_123' },
});
// Activate license (no API key required)
const { data: activation } = await activateLicense('LICENSE-KEY-HERE', 'instance-name');
console.log('License valid:', activation.valid);
console.log('Activations:', activation.license_key.activation_usage);
// Validate license (no API key required)
const { data: validation } = await validateLicense('LICENSE-KEY-HERE', 'instance-name');
if (validation.valid) {
console.log('License is active');
} else {
console.log('Invalid:', validation.error);
}
// Deactivate license (no API key required)
await deactivateLicense('LICENSE-KEY-HERE', 'instance-id');
// Disable/enable license key (requires API key)
await updateLicenseKey('lk_123', {
disabled: true,
});
Orders
import { listOrders, getOrder, listOrderItems, generateOrderInvoice } from '@lemonsqueezy/lemonsqueezy.js';
// List orders
const { data: orders } = await listOrders({
filter: {
storeId: process.env.LEMONSQUEEZY_STORE_ID,
userEmail: 'customer@example.com',
},
});
// Get order details
const { data: order } = await getOrder('order_123');
console.log('Total:', order.attributes.total_formatted);
console.log('Status:', order.attributes.status);
// Get order items
const { data: items } = await listOrderItems({
filter: { orderId: 'order_123' },
});
// Generate invoice PDF
const { data: invoice } = await generateOrderInvoice('order_123');
console.log('Invoice URL:', invoice.attributes.url);
Customers
import {
createCustomer,
getCustomer,
listCustomers,
updateCustomer,
archiveCustomer,
} from '@lemonsqueezy/lemonsqueezy.js';
// Create customer
const { data: customer } = await createCustomer(process.env.LEMONSQUEEZY_STORE_ID!, {
name: 'John Doe',
email: 'john@example.com',
city: 'New York',
country: 'US',
});
// List customers
const { data: customers } = await listCustomers({
filter: { storeId: process.env.LEMONSQUEEZY_STORE_ID },
});
// Update customer
await updateCustomer('cust_123', {
name: 'Jane Doe',
});
// Archive customer
await archiveCustomer('cust_123');
Discounts
import {
createDiscount,
listDiscounts,
getDiscount,
deleteDiscount,
} from '@lemonsqueezy/lemonsqueezy.js';
// Create discount
const { data: discount } = await createDiscount(process.env.LEMONSQUEEZY_STORE_ID!, {
name: 'Summer Sale',
code: 'SUMMER2024',
amount: 20,
amountType: 'percent', // or 'fixed'
isLimitedToProducts: false,
isLimitedRedemptions: true,
maxRedemptions: 100,
startsAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
});
// List discounts
const { data: discounts } = await listDiscounts({
filter: { storeId: process.env.LEMONSQUEEZY_STORE_ID },
});
// Delete discount
await deleteDiscount('disc_123');
Webhooks
Setup Webhook Handler
import { createWebhook, listWebhooks } from '@lemonsqueezy/lemonsqueezy.js';
import crypto from 'crypto';
// Create webhook programmatically
const { data: webhook } = await createWebhook(process.env.LEMONSQUEEZY_STORE_ID!, {
url: 'https://myapp.com/api/webhooks/lemonsqueezy',
events: [
'order_created',
'subscription_created',
'subscription_updated',
'subscription_cancelled',
'subscription_payment_success',
'subscription_payment_failed',
'license_key_created',
],
secret: process.env.LEMONSQUEEZY_WEBHOOK_SECRET,
});
Handle Webhooks (Next.js)
// app/api/webhooks/lemonsqueezy/route.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
export async function POST(request: NextRequest) {
const rawBody = await request.text();
const signature = request.headers.get('x-signature');
// Verify signature
const hmac = crypto.createHmac('sha256', process.env.LEMONSQUEEZY_WEBHOOK_SECRET!);
const digest = hmac.update(rawBody).digest('hex');
if (signature !== digest) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
const payload = JSON.parse(rawBody);
const eventName = payload.meta.event_name;
const data = payload.data;
switch (eventName) {
case 'order_created':
console.log('New order:', data.id);
// Fulfill order, send email, etc.
break;
case 'subscription_created':
console.log('New subscription:', data.id);
// Grant access, update database
break;
case 'subscription_updated':
console.log('Subscription updated:', data.attributes.status);
// Handle plan changes, pauses
break;
case 'subscription_cancelled':
console.log('Subscription cancelled:', data.id);
// Revoke access, send win-back email
break;
case 'subscription_payment_success':
console.log('Payment received');
// Update billing records
break;
case 'subscription_payment_failed':
console.log('Payment failed');
// Notify customer, retry logic
break;
case 'license_key_created':
console.log('License created:', data.attributes.key);
// Send license to customer
break;
}
return NextResponse.json({ received: true });
}
Lemon.js (Frontend)
For client-side checkout overlays.
<script src="https://app.lemonsqueezy.com/js/lemon.js" defer></script>
<a href="https://mystore.lemonsqueezy.com/checkout/buy/abc123" class="lemonsqueezy-button">
Buy Now
</a>
With JavaScript
// Initialize
window.createLemonSqueezy();
// Open checkout programmatically
window.LemonSqueezy.Url.Open('https://mystore.lemonsqueezy.com/checkout/buy/abc123');
// Listen to events
window.LemonSqueezy.Setup({
eventHandler: (event) => {
if (event.event === 'Checkout.Success') {
console.log('Purchase complete!', event.data);
}
},
});
Usage Records (Metered Billing)
import { createUsageRecord, listUsageRecords } from '@lemonsqueezy/lemonsqueezy.js';
// Report usage
const { data: record } = await createUsageRecord({
subscriptionItemId: 'si_123',
quantity: 100,
action: 'increment', // or 'set'
});
// List usage records
const { data: records } = await listUsageRecords({
filter: { subscriptionItemId: 'si_123' },
});
Next.js Server Actions
// app/actions/billing.ts
'use server';
import {
createCheckout,
cancelSubscription,
updateSubscription,
} from '@lemonsqueezy/lemonsqueezy.js';
import { auth } from '@/lib/auth';
export async function createCheckoutAction(variantId: string) {
const session = await auth();
if (!session?.user) throw new Error('Unauthorized');
const { data, error } = await createCheckout(
process.env.LEMONSQUEEZY_STORE_ID!,
{
checkoutData: {
email: session.user.email,
custom: { userId: session.user.id },
},
},
{
relationships: {
variant: { data: { type: 'variants', id: variantId } },
},
}
);
if (error) throw new Error(error.message);
return data.attributes.url;
}
export async function cancelSubscriptionAction(subscriptionId: string) {
const session = await auth();
if (!session?.user) throw new Error('Unauthorized');
// Verify ownership before cancelling
await cancelSubscription(subscriptionId);
return { success: true };
}
Environment Variables
LEMONSQUEEZY_API_KEY=your_api_key
LEMONSQUEEZY_STORE_ID=your_store_id
LEMONSQUEEZY_WEBHOOK_SECRET=your_webhook_secret
Best Practices
- Server-side only - Never expose API key in browser
- Verify webhooks - Always validate signatures
- Use test mode - Build integration before going live
- Store custom data - Pass userId in checkout for correlation
- Handle all events - Account for failed payments, cancellations
- Idempotent handlers - Webhooks may be delivered multiple times