Skip to Content
DocsPaymentLemonSqueezy Payments

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

  1. Go to lemonsqueezy.com  and create an account
  2. Complete your store setup and verification
  3. 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

  1. Go to Products in your LemonSqueezy dashboard
  2. Create a new product (e.g., “Pro Plan”)
  3. Add pricing and billing details
  4. Copy the Variant ID (NOT the Product ID)

3. Set Up Webhooks

  1. Go to Settings → Webhooks in LemonSqueezy dashboard
  2. Add webhook URL: https://your-domain.com/api/lemonsqueezy/webhook
  3. Enable these events:
    • subscription_created
    • subscription_updated
    • order_created
    • subscription_cancelled
    • subscription_expired
  4. 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:3000

5. How It Works in the Template

The template includes complete LemonSqueezy integration:

  • API Client: lib/lemonsqueezy.ts configured 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 LemonSqueezy

Payment 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 required

Subscription 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 status
  • GET /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

  1. Start Development Server:

    npm run dev
  2. 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
  3. 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
  4. Verify Database Records:

    • For Prisma: npm run db:studio
    • For Supabase: Check Supabase dashboard
    • Verify subscription and payment records were created

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 completion
  • subscription_created - New subscription setup
  • subscription_updated - Status and billing changes
  • subscription_cancelled - Cancellation handling
  • subscription_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

  1. In LemonSqueezy dashboard, ensure your store is approved for live transactions
  2. Get your live API credentials
  3. Update environment variables with live keys

2. Production Webhooks

  1. Update webhook URL to your production domain
  2. Ensure webhook secret is set in production environment
  3. 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.com

Advanced 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:generate after 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
Last updated on