Lemon Squeezy is a payment platform designed for digital products and SaaS. Its standout feature is acting as a Merchant of Record (MoR), handling VAT and sales tax compliance globally.
Prerequisites
- A Lemon Squeezy account (sign up here)
- Your ShipFree application running locally or deployed
- A verified store in Lemon Squeezy
What is Merchant of Record?
As a Merchant of Record, Lemon Squeezy:
- Handles all tax compliance: VAT, sales tax, GST worldwide
- Is the seller of record: Your customers buy from Lemon Squeezy, not you
- Manages invoicing: Generates proper tax invoices
- Handles refunds and chargebacks: Takes on the liability
This simplifies your business but means Lemon Squeezy takes a higher fee (5% + payment processing).
Setup Instructions
Step 1: Set Up Your Store
- Log in to Lemon Squeezy
- Complete store verification (required for payouts)
- Set up your payout method
- Configure tax settings (automatically handled as MoR)
Step 2: Get Your API Key and Store ID
- Go to Settings → API
- Click Create API Key
- Copy your API key (starts with
eyJ0eXAiOi...)
- Note your Store ID (found in Settings)
Step 3: Create Products and Variants
Lemon Squeezy uses products and variants for pricing.
-
Go to Products in your dashboard
-
Click Create Product
-
For each plan (Starter, Pro, Enterprise):
- Set product name and description
- Set product type to Subscription
- Add product image (optional)
- Click Create Product
-
Create variants for each billing interval:
- Click Add Variant
- Set variant name (e.g., “Monthly”)
- Set price (e.g., $9.90)
- Set billing interval: Monthly or Yearly
- Enable Subscription
- Save and copy the Variant ID (numeric ID)
In Lemon Squeezy, you use Variant IDs (not Product IDs) when creating checkouts. Each variant represents a specific price point and billing interval.
Add these variables to your .env file:
# Payment Provider Selection
PAYMENT_PROVIDER=lemonsqueezy
# Lemon Squeezy API Key (server-side)
LEMONSQUEEZY_API_KEY=eyJ0eXAiOiJKV1QiLCJhbGc...
# Lemon Squeezy Store ID
LEMONSQUEEZY_STORE_ID=12345
# Lemon Squeezy Webhook Secret (from Step 5)
LEMONSQUEEZY_WEBHOOK_SECRET=your_webhook_secret_here
# Lemon Squeezy Variant IDs (client-side, for display)
# Note: In LS, we use variant IDs, not product IDs
NEXT_PUBLIC_LEMONSQUEEZY_PRODUCT_STARTER_MONTHLY=123456
NEXT_PUBLIC_LEMONSQUEEZY_PRODUCT_PRO_MONTHLY=123457
NEXT_PUBLIC_LEMONSQUEEZY_PRODUCT_ENTERPRISE_MONTHLY=123458
In ShipFree’s config, these are named PRODUCT_* but actually contain Variant IDs for Lemon Squeezy. This maintains consistency across providers.
Step 5: Set Up Webhooks
Lemon Squeezy uses webhooks to notify your application about subscription events.
For Local Development
Use ngrok to expose your local server:
-
Install ngrok:
# macOS
brew install ngrok/ngrok/ngrok
-
Start your development server:
-
In another terminal, start ngrok:
-
Copy the HTTPS URL (e.g.,
https://abc123.ngrok.io)
-
In Lemon Squeezy dashboard, go to Settings → Webhooks
-
Click Add Webhook:
- URL:
https://abc123.ngrok.io/api/webhooks/payments
- Signing Secret: Generate a random string (save this!)
- Events: Select all subscription and order events
-
Add the signing secret to your
.env:
LEMONSQUEEZY_WEBHOOK_SECRET=your_generated_secret
For Production
-
Go to Settings → Webhooks
-
Click Add Webhook
-
Set webhook URL:
https://yourdomain.com/api/webhooks/payments
-
Generate and save a signing secret
-
Select events:
subscription_created
subscription_updated
subscription_cancelled
subscription_expired
subscription_payment_success
order_created
-
Add signing secret to production environment variables
Testing in Development
Test Mode
Lemon Squeezy provides test mode for development:
- In your dashboard, toggle Test Mode (top right)
- Create test products and variants
- Use test checkout flows
Test Checkout
Lemon Squeezy provides test card numbers:
| Card Number | Scenario |
|---|
4242 4242 4242 4242 | Successful payment |
4000 0000 0000 0002 | Declined (generic) |
- Use any future expiration date
- Use any 3-digit CVC
Testing Subscriptions
-
Start your development server and ngrok:
bun dev
# In another terminal:
ngrok http 3000
-
Configure webhook in Lemon Squeezy with your ngrok URL
-
Create a checkout:
import { getPaymentAdapter } from '@/lib/payments/service'
const adapter = getPaymentAdapter()
const checkout = await adapter.createCheckout({
plan: 'pro',
userId: 'user_123',
email: 'test@example.com',
successUrl: 'http://localhost:3000/dashboard?checkout=success',
})
console.log('Checkout URL:', checkout.url)
-
Complete the test checkout
-
Verify webhook events in your application logs
Lemon Squeezy Implementation Details
The Lemon Squeezy adapter is implemented in src/lib/payments/providers/lemonsqueezy.ts.
Key Features
Checkout Sessions
Creates Lemon Squeezy checkouts with:
- Variant-based pricing (not product-based)
- Customer metadata
- Custom redirect URLs
- Trial periods (if configured)
async createCheckout(options: CheckoutOptions): Promise<CheckoutResult> {
const newCheckout: NewCheckout = {
productOptions: {
redirectUrl: successUrl || paymentConfig.providers.successUrl,
receiptButtonText: 'Go to Dashboard',
receiptLinkUrl: paymentConfig.providers.successUrl,
},
checkoutData: {
email,
custom: {
userId,
plan,
provider: 'lemonsqueezy',
},
},
}
const { data, error } = await createCheckout(
storeId,
Number.parseInt(price.productId), // Variant ID
newCheckout
)
return {
url: data.data.attributes.url,
sessionId: data.data.id,
}
}
Customer Management
Lemon Squeezy creates customers automatically during checkout:
- Customers are created on first purchase
- Customer data includes email and metadata
- Uses customer lookup by email
async createCustomer(userId: string, email?: string): Promise<CustomerData> {
if (email) {
const { data } = await listCustomers({
filter: { email },
page: { size: 1 },
})
if (data?.data && data.data.length > 0) {
const customer = data.data[0]
return {
id: `ls_${customer.id}`,
providerCustomerId: customer.id,
email: customer.attributes.email,
userId,
provider: 'lemonsqueezy',
}
}
}
// Return placeholder - real customer created at checkout
return {
id: `ls_pending_${userId}`,
providerCustomerId: `ls_pending_${userId}`,
email,
userId,
provider: 'lemonsqueezy',
}
}
Subscription Handling
Provides:
- Subscription retrieval by ID
- Status mapping to unified format
- Cancellation support (always at period end)
async cancelSubscription(
providerSubscriptionId: string,
cancelAtPeriodEnd = true
): Promise<void> {
// Lemon Squeezy only supports cancelling at period end via API
const { error } = await cancelSubscription(providerSubscriptionId)
if (error) {
throw new Error(`Failed to cancel subscription: ${error.message}`)
}
}
Webhook Processing
Processes these events:
subscription_created, subscription_updated, subscription_cancelled, subscription_expired
order_created
Webhook validation using HMAC:
async validateWebhook(rawBody: string, signature: string): Promise<boolean> {
const hmac = crypto.createHmac('sha256', process.env.LEMONSQUEEZY_WEBHOOK_SECRET!)
const digest = Buffer.from(hmac.update(rawBody).digest('hex'), 'utf8')
const signatureBuffer = Buffer.from(signature, 'utf8')
return crypto.timingSafeEqual(digest, signatureBuffer)
}
Customer Portal
Lemon Squeezy provides a customer portal accessible via magic link:
const portal = await adapter.createPortal(
'customer_email@example.com',
'https://yourdomain.com/dashboard'
)
// Redirect to portal.url
The portal allows customers to:
- Update payment methods
- View invoices and receipts
- Cancel subscriptions
- Download tax invoices
You can pass custom data in checkouts to track users:
checkoutData: {
custom: {
userId: 'user_123',
plan: 'pro',
// Add any custom fields
teamId: 'team_456',
},
}
This data is returned in webhook events as meta.custom_data.
Going Live
Pre-Launch Checklist
Tax Compliance
As a Merchant of Record, Lemon Squeezy handles:
- EU VAT: All 27 EU countries
- US Sales Tax: All applicable states
- UK VAT
- Canadian GST/HST/PST
- Australian GST
- And more: Check Lemon Squeezy tax coverage
You don’t need to:
- Register for tax IDs in different countries
- Calculate tax rates
- File tax returns
- Generate compliant invoices
Security Best Practices
- Protect API keys: Keep
LEMONSQUEEZY_API_KEY server-side only
- Validate webhooks: Always verify HMAC signatures
- Use HTTPS: Required for production webhooks
- Secure webhook secret: Use a strong random string
- Monitor webhook logs: Check for failed events
Troubleshooting
Webhook Not Received
- Verify ngrok is running (local dev)
- Check webhook endpoint is accessible
- Review webhook logs in Lemon Squeezy dashboard
- Ensure
LEMONSQUEEZY_WEBHOOK_SECRET matches dashboard
- Check signature validation logic
Checkout Session Fails
- Verify
LEMONSQUEEZY_API_KEY is set
- Check
LEMONSQUEEZY_STORE_ID is correct
- Ensure variant IDs exist and are active
- Check that products are published
- Review error response from Lemon Squeezy API
Subscription Not Created
- Check webhook was sent (Lemon Squeezy dashboard)
- Verify webhook signature validation passes
- Review application logs for errors
- Ensure custom_data includes userId
- Check database for subscription records
Customer Portal Issues
- Ensure customer email is correct
- Verify customer exists in Lemon Squeezy
- Check customer has active subscription
- Try accessing portal directly from Lemon Squeezy dashboard
Lemon Squeezy vs Other Providers
When to Choose Lemon Squeezy
✅ Choose Lemon Squeezy if you:
- Want hassle-free global tax compliance
- Are selling digital products or SaaS
- Don’t want to deal with tax registrations
- Prefer simple setup and pricing
- Are a solo founder or small team
- Want Lemon Squeezy to handle refunds/chargebacks
❌ Avoid Lemon Squeezy if you:
- Need to be the merchant of record (legal/branding reasons)
- Want maximum control over payment flow
- Need extensive payment methods
- Require advanced billing features (metered usage, etc.)
- Want lowest possible fees
Feature Comparison
| Feature | Lemon Squeezy | Stripe | Polar |
|---|
| Merchant of Record | ✅ Yes | ❌ No | ❌ No |
| Tax Handling | ✅ Automatic | ⚠️ Manual | ⚠️ Manual |
| Setup Complexity | ✅ Low | ❌ High | ✅ Low |
| Fees | ⚠️ 5% + processing | ⚠️ 2.9% + $0.30 | ✅ Competitive |
| Payment Methods | ⚠️ Good | ✅ Extensive | ⚠️ Basic |
| Customer Portal | ✅ Good | ✅ Advanced | ⚠️ Basic |
| Best For | Digital products | Enterprises | Developer tools |
Additional Resources
Support
For Lemon Squeezy-specific issues:
For ShipFree integration issues:
- Check the source code in
src/lib/payments/providers/lemonsqueezy.ts
- Review webhook handling in
src/app/api/webhooks/payments/route.ts