Loading...
Loading...
> Accept global payments with automatic tax handling and merchant of record.
Lemon Squeezy is a merchant of record (MoR) that handles sales tax, VAT, and payment processing globally. Unlike Stripe where you're the merchant, Lemon Squeezy is the seller on your behalf - they handle all tax compliance, refunds, and chargebacks. Perfect for selling to customers worldwide without tax headaches.
DESC: Go to lemonsqueezy.com and create a store. Complete your onboarding to enable payments.
DESC: In Settings → API, create a new API key with full access. Copy your Store ID from the store settings.
DESC: Go to Products and create items for each tier. Each product variant has a unique ID you'll use for checkout.
DESC: Add these to your .env.local file
1$# Lemon Squeezy API Keys2$LEMONSQUEEZY_API_KEY="your_api_key_here"3$LEMONSQUEEZY_STORE_ID="your_store_id"4$LEMONSQUEEZY_WEBHOOK_SECRET="your_webhook_secret"56$# Product Variant IDs (from Lemon Squeezy dashboard)7$NEXT_PUBLIC_LEMONSQUEEZY_VARIANT_STARTER="123456"8$NEXT_PUBLIC_LEMONSQUEEZY_VARIANT_PRO="123457"9$NEXT_PUBLIC_LEMONSQUEEZY_VARIANT_ENTERPRISE="123458"
DESC: In Settings → Webhooks, create a webhook pointing to your application's endpoint.
1$# Production webhook URL2$https://your-domain.com/api/lemonsqueezy/webhook34$# Events to subscribe to:5$# - order_created6$# - subscription_created7$# - subscription_updated8$# - subscription_cancelled9$# - subscription_payment_success10$# - subscription_payment_failed
Tip: Copy the webhook signing secret and add it as LEMONSQUEEZY_WEBHOOK_SECRET
Create a checkout session and redirect to Lemon Squeezy
1"use client";23import { useState } from "react";4import { Button } from "@/components/ui/button";56export function LemonSqueezyCheckout({ variantId, planName }) {7 const [loading, setLoading] = useState(false);89 const handleCheckout = async () => {10 setLoading(true);11 try {12 const response = await fetch("/api/lemonsqueezy/checkout", {13 method: "POST",14 headers: { "Content-Type": "application/json" },15 body: JSON.stringify({ variantId }),16 });1718 const { url, error } = await response.json();1920 if (error) {21 alert(error);22 return;23 }2425 // Redirect to Lemon Squeezy Checkout26 window.location.href = url;27 } catch (err) {28 alert("Something went wrong. Please try again.");29 } finally {30 setLoading(false);31 }32 };3334 return (35 <Button onClick={handleCheckout} disabled={loading}>36 {loading ? "Loading..." : `Get ${planName}`}37 </Button>38 );39}
Server-side checkout creation
1// src/app/api/lemonsqueezy/checkout/route.ts2import { NextResponse } from "next/server";3import { auth } from "@/lib/auth";4import { createLemonSqueezyCheckout } from "@/lib/lemonsqueezy";5import { env } from "@/lib/env";67export async function POST(req: Request) {8 const session = await auth();9 if (!session?.user) {10 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });11 }1213 const { variantId } = await req.json();1415 try {16 const checkoutUrl = await createLemonSqueezyCheckout({17 variantId,18 email: session.user.email!,19 userId: session.user.id,20 successUrl: `${env.client.NEXT_PUBLIC_APP_URL}/dashboard?success=true`,21 cancelUrl: `${env.client.NEXT_PUBLIC_APP_URL}/pricing`,22 });2324 return NextResponse.json({ url: checkoutUrl });25 } catch (error) {26 console.error("Lemon Squeezy checkout error:", error);27 return NextResponse.json(28 { error: "Failed to create checkout" },29 { status: 500 }30 );31 }32}
Handle Lemon Squeezy webhook events
1// src/app/api/lemonsqueezy/webhook/route.ts2import { NextResponse } from "next/server";3import crypto from "crypto";4import { handleLemonSqueezyWebhook } from "@/lib/lemonsqueezy";5import { env } from "@/lib/env";67export async function POST(req: Request) {8 const body = await req.text();9 const signature = req.headers.get("X-Signature");1011 // Verify webhook signature12 const hmac = crypto.createHmac("sha256", env.server.LEMONSQUEEZY_WEBHOOK_SECRET);13 const digest = hmac.update(body).digest("hex");1415 if (signature !== digest) {16 return NextResponse.json({ error: "Invalid signature" }, { status: 401 });17 }1819 const event = JSON.parse(body);2021 try {22 await handleLemonSqueezyWebhook(event);23 return NextResponse.json({ received: true });24 } catch (error) {25 console.error("Webhook error:", error);26 return NextResponse.json({ error: "Webhook failed" }, { status: 500 });27 }28}
When to use Lemon Squeezy
order_createdNew purchase completedsubscription_createdNew subscription startedsubscription_updatedPlan changed or modifiedsubscription_cancelledSubscription endedsubscription_payment_failedPayment attempt failed