LemonSqueezy Payments
LemonSqueezy provides simple payment processing with built-in tax handling and EU VAT compliance. This module is already integrated in the template.
Overview
LemonSqueezy offers simplified payment processing with automatic tax compliance, subscription management, and developer-friendly APIs.
Key Features:
- Simple subscription billing
- Automatic tax handling worldwide
- Built-in EU VAT compliance
- Easy checkout integration
- Webhook notifications
- Customer portal
Getting Started
1. Get Your LemonSqueezy Credentials
- Go to lemonsqueezy.com and create an account
- Complete your store setup and verification
- Create your products and get these credentials:
- API Key: Go to Settings → API
- Store ID: Go to Settings → Stores
- Product Variant ID: Go to Products → Your Product → Variants
2. Set Up Products
- Go to Products in your LemonSqueezy dashboard
- Create a new product (e.g., “Pro Plan”)
- Add pricing and billing details
- Copy the Variant ID (NOT the Product ID)
3. Set Up Webhooks
- Go to Settings → Webhooks in LemonSqueezy dashboard
- Add webhook URL:
https://your-domain.com/api/lemonsqueezy/webhook - Enable these events:
subscription_createdsubscription_updatedorder_createdsubscription_cancelledsubscription_expired
- Copy the webhook signing secret
4. Add Environment Variables
Add these to your .env.local file:
LEMON_SQUEEZY_API_KEY=lmsq_api_your_api_key_here
LEMON_SQUEEZY_STORE_ID=your_store_id_here
NEXT_PUBLIC_LEMON_SQUEEZY_PRO_PRODUCT_ID=your_variant_id_here
LEMON_SQUEEZY_WEBHOOK_SECRET=your_webhook_secret_here
NEXT_PUBLIC_APP_URL=http://localhost:30005. How It Works in the Template
The template includes complete LemonSqueezy integration:
- API Client:
lib/lemonsqueezy.tsconfigured with authentication - Checkout API: Creates secure checkout sessions
- Webhook Handler: Processes payment events and updates database
- Database Integration: Automatic subscription and payment tracking
- Configuration System: Centralized config management
6. Template Structure
lib/
├── lemonsqueezy.ts # LemonSqueezy API client and checkout
├── config.ts # Configuration management
app/api/
├── checkout/ # Create LemonSqueezy checkout sessions
└── lemonsqueezy/webhook/ # Handle payment webhooks
components/ui/
└── pricing-cards.tsx # Integrated pricing with LemonSqueezyPayment Flow
The template handles the complete payment flow:
1. User Clicks Subscribe
When users click a subscription button:
// This is already implemented in pricing-cards.tsx
const handleSubscribe = async () => {
const response = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productId: process.env.NEXT_PUBLIC_LEMON_SQUEEZY_PRO_PRODUCT_ID
})
})
const { checkoutUrl } = await response.json()
// Redirect to LemonSqueezy checkout
window.location.href = checkoutUrl
}2. Checkout Session Creation
The template creates secure checkout sessions:
// app/api/checkout/route.ts (already implemented)
export async function POST(req: Request) {
const { userId } = await auth() // Clerk authentication
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { productId } = await req.json()
// Get user from database
const user = await prisma.user.findUnique({
where: { clerkId: userId }
})
// Create LemonSqueezy checkout session
const checkoutUrl = await createCheckoutSession(productId, user.id)
return NextResponse.json({ checkoutUrl })
}3. LemonSqueezy Checkout Creation
The template creates checkout sessions:
// lib/lemonsqueezy.ts (already implemented)
export const createCheckoutSession = async (productId: string, userId: string) => {
const response = await lemonSqueezyApiInstance.post('/checkouts', {
data: {
type: 'checkouts',
attributes: {
checkout_data: {
custom: { user_id: userId }
},
checkout_options: {
embed: false,
media: false,
logo: true
},
product_options: {
enabled_variants: [productId],
redirect_url: `${config.app.url}/dashboard?payment=success`,
receipt_button_text: "Go to Dashboard",
receipt_thank_you_note: "Thank you for upgrading to Pro!"
}
},
relationships: {
store: { data: { type: 'stores', id: storeId } },
variant: { data: { type: 'variants', id: productId } }
}
}
})
return response.data.data.attributes.url
}4. Webhook Processing
The template automatically handles webhooks:
// app/api/lemonsqueezy/webhook/route.ts (already implemented)
export async function POST(request: Request) {
const body = await request.text()
const signature = request.headers.get('X-Signature')
// Verify webhook signature
const hmac = crypto.createHmac('sha256', webhookSecret!)
const digest = hmac.update(body).digest('hex')
const expectedSignature = `sha256=${digest}`
if (signature !== expectedSignature) {
return Response.json({ error: 'Invalid signature' }, { status: 400 })
}
const event = JSON.parse(body)
switch (event.meta.event_name) {
case 'order_created':
await handleOrderCreated(event)
break
case 'subscription_created':
await handleSubscriptionCreated(event)
break
case 'subscription_updated':
await handleSubscriptionUpdated(event)
break
}
return Response.json({ received: true })
}Database Integration
The template automatically manages:
User Records
// Users are automatically synced from Clerk authentication
// No additional setup requiredSubscription Records (for Prisma templates)
// Subscriptions are created/updated via webhooks
model Subscription {
id String @id @default(cuid())
userId String
lemonSqueezyId String? @unique
status SubscriptionStatus @default(INACTIVE)
planType String?
currentPeriodEnd DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}Subscription Records (for Supabase templates)
-- Subscriptions are tracked in Supabase
CREATE TABLE public.subscriptions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
lemon_squeezy_id TEXT UNIQUE NOT NULL,
status TEXT NOT NULL, -- active, cancelled, expired
plan_type TEXT NOT NULL,
current_period_end TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);Payment Records
// Payments are tracked automatically
model Payment {
id String @id @default(cuid())
userId String
lemonSqueezyId String? @unique
amount Int // Amount in cents
currency String @default("usd")
status PaymentStatus @default(PENDING)
description String?
}API Endpoints
The template includes these payment APIs:
Checkout API (/api/checkout)
- POST - Create LemonSqueezy checkout session
- Validates user authentication
- Gets user from database
- Creates checkout URL with custom data
- Returns checkout URL for redirect
Webhook API (/api/lemonsqueezy/webhook)
- POST - Process LemonSqueezy webhooks
- Verifies webhook signatures
- Updates database based on events
- Handles all subscription lifecycle events
User APIs (inherited from base template)
GET /api/user/subscription- Get subscription statusGET /api/user/payments- Get payment history
Testing Payments
Test Cards
LemonSqueezy provides test mode automatically:
- Success:
4242 4242 4242 4242 - Declined:
4000 0000 0000 0002
Use any future expiry date and any 3-digit CVC.
Testing Workflow
-
Start Development Server:
npm run dev -
Set Up Webhook Testing (for local development):
# Use ngrok to expose your local server npx ngrok http 3000 # Update webhook URL in LemonSqueezy dashboard to: # https://your-ngrok-url.ngrok.io/api/lemonsqueezy/webhook -
Test Payment Flow:
- Sign up/Sign in to your app
- Go to pricing page
- Click “Subscribe to Pro” button
- Complete checkout with test card
- Verify success redirect to dashboard
-
Verify Database Records:
- For Prisma:
npm run db:studio - For Supabase: Check Supabase dashboard
- Verify subscription and payment records were created
- For Prisma:
Subscription Management
Get Subscription Status
// For Prisma templates
const subscription = await prisma.subscription.findFirst({
where: {
userId: user.id,
status: 'ACTIVE'
}
})
// For Supabase templates
const { data: subscription } = await supabase
.from('subscriptions')
.select('*')
.eq('user_id', user.id)
.eq('status', 'active')
.single()Cancel Subscription
// LemonSqueezy provides API for subscription management
const cancelSubscription = async (subscriptionId: string) => {
const response = await lemonSqueezyApiInstance.delete(
`/subscriptions/${subscriptionId}`
)
return response.data
}Pricing Integration
The template includes pricing cards with integrated LemonSqueezy checkout:
// components/ui/pricing-cards.tsx (already implemented)
const plans = [
{
name: 'Starter',
price: '$0',
productId: null,
features: ['Basic features', '5 projects']
},
{
name: 'Pro',
price: '$49/month',
productId: process.env.NEXT_PUBLIC_LEMON_SQUEEZY_PRO_PRODUCT_ID,
features: ['Everything in Starter', 'Unlimited projects']
}
]
// Checkout button automatically integrated
<LemonSqueezyButton
productId={plan.productId}
disabled={!isSignedIn}
>
Subscribe to {plan.name}
</LemonSqueezyButton>Webhook Events Handled
The template processes these webhook events:
order_created- Initial payment completionsubscription_created- New subscription setupsubscription_updated- Status and billing changessubscription_cancelled- Cancellation handlingsubscription_expired- Expiration handling
Configuration System
The template includes centralized configuration:
// lib/config.ts (already implemented)
export const config = {
app: {
url: process.env.NEXT_PUBLIC_APP_URL!
},
lemonSqueezy: {
apiKey: process.env.LEMON_SQUEEZY_API_KEY!,
storeId: process.env.LEMON_SQUEEZY_STORE_ID!,
proProductId: process.env.NEXT_PUBLIC_LEMON_SQUEEZY_PRO_PRODUCT_ID!,
webhookSecret: process.env.LEMON_SQUEEZY_WEBHOOK_SECRET!
}
}
// Debug configuration in development
export const debugConfig = () => {
console.log('🍋 LemonSqueezy Configuration:')
console.log('Store ID:', config.lemonSqueezy.storeId)
console.log('Product ID:', config.lemonSqueezy.proProductId)
console.log('API Key present:', !!config.lemonSqueezy.apiKey)
}Production Setup
1. Live Mode Setup
- In LemonSqueezy dashboard, ensure your store is approved for live transactions
- Get your live API credentials
- Update environment variables with live keys
2. Production Webhooks
- Update webhook URL to your production domain
- Ensure webhook secret is set in production environment
- Test webhook delivery in LemonSqueezy dashboard
3. Environment Variables
# Production environment
LEMON_SQUEEZY_API_KEY=lmsq_api_your_live_api_key
LEMON_SQUEEZY_STORE_ID=your_live_store_id
NEXT_PUBLIC_LEMON_SQUEEZY_PRO_PRODUCT_ID=your_live_variant_id
LEMON_SQUEEZY_WEBHOOK_SECRET=your_live_webhook_secret
NEXT_PUBLIC_APP_URL=https://yourdomain.comAdvanced Features
Custom Checkout Data
// Pass custom data to LemonSqueezy checkout
const checkoutSession = {
checkout_data: {
custom: {
user_id: userId,
plan_type: 'pro',
source: 'website'
}
}
}Discount Codes
// Apply discount codes during checkout
product_options: {
enabled_variants: [productId],
redirect_url: successUrl,
discount_codes: ['SAVE20'] // Pre-apply discount
}Receipt Customization
// Customize post-purchase experience
product_options: {
receipt_button_text: "Access Your Dashboard",
receipt_thank_you_note: "Welcome to Pro! Your subscription is now active.",
redirect_url: `${appUrl}/dashboard?welcome=true`
}Troubleshooting
Checkout not working:
- Verify all environment variables are set correctly
- Check that variant ID (not product ID) is being used
- Ensure user is authenticated before checkout attempt
- Verify API key has correct permissions
Webhook verification failing:
- Check webhook secret matches LemonSqueezy dashboard exactly
- Ensure webhook URL is accessible from LemonSqueezy servers
- Verify request body is being read as text, not JSON
- Check for any middleware modifying the request
Subscription not updating:
- Verify webhook events are being received
- Check webhook signature verification is working
- Look at webhook logs in LemonSqueezy dashboard
- Ensure database operations are succeeding
Configuration issues:
- Run in development mode to see debug configuration output
- Verify all required environment variables are present
- Check that store is properly set up in LemonSqueezy
- Ensure products and variants are correctly configured
Database integration issues:
- For Prisma: Run
npm run db:generateafter schema changes - For Supabase: Check RLS policies allow webhook operations
- Verify user ID mapping between authentication and payments
- Check database logs for constraint violations or errors
API errors:
- Check LemonSqueezy API status page
- Verify API key permissions and limits
- Look at detailed error responses in server logs
- Test API credentials with simple API calls first