Lemon Squeezy Integration
ShipKit integrates with Lemon Squeezy for handling payments, subscriptions, and digital product delivery. This guide covers setup, payment flows, and webhook handling.
Setup
Installation
pnpm add @lemonsqueezy/lemonsqueezy.js
Configuration
// src/lib/lemon-squeezy.ts
import { LemonSqueezy } from '@lemonsqueezy/lemonsqueezy.js';
import { env } from '@/env';
export const lemonSqueezy = new LemonSqueezy(env.LEMON_SQUEEZY_API_KEY);
export const storeConfig = {
storeId: env.LEMON_SQUEEZY_STORE_ID,
apiVersion: 'v1',
mode: process.env.NODE_ENV === 'production' ? 'live' : 'test',
} as const;
Payment Flows
Checkout Integration
// src/components/checkout-button.tsx
'use client';
import { useState } from 'react';
import { createCheckout } from '@lemonsqueezy/lemonsqueezy.js';
import { Button } from '@/components/ui/button';
interface CheckoutButtonProps {
variantId: number;
productName: string;
checkoutOptions?: {
email?: string;
name?: string;
customData?: Record<string, any>;
};
}
export const CheckoutButton = ({
variantId,
productName,
checkoutOptions,
}: CheckoutButtonProps) => {
const [loading, setLoading] = useState(false);
const handleCheckout = async () => {
setLoading(true);
try {
const checkout = await createCheckout({
storeId: storeConfig.storeId,
variantId,
checkoutOptions: {
...checkoutOptions,
darkMode: false,
media: false,
},
});
window.location.href = checkout.url;
} catch (error) {
console.error('Checkout error:', error);
} finally {
setLoading(false);
}
};
return (
<Button
onClick={handleCheckout}
disabled={loading}
>
{loading ? 'Loading...' : `Buy ${productName}`}
</Button>
);
};
Custom Checkout Page
// src/app/(checkout)/checkout/[variantId]/page.tsx
import { getProductVariant } from '@/lib/lemon-squeezy';
import { CheckoutForm } from '@/components/checkout-form';
interface CheckoutPageProps {
params: {
variantId: string;
};
}
export default async function CheckoutPage({
params,
}: CheckoutPageProps) {
const variant = await getProductVariant(params.variantId);
return (
<div className="container max-w-lg py-12">
<h1 className="text-2xl font-bold mb-6">
Checkout: {variant.attributes.name}
</h1>
<CheckoutForm
variant={variant}
onSuccess={(checkoutUrl) => {
window.location.href = checkoutUrl;
}}
/>
</div>
);
}
Subscription Management
Subscription Status
// src/lib/subscriptions.ts
import { lemonSqueezy } from '@/lib/lemon-squeezy';
export async function getSubscriptionStatus(subscriptionId: string) {
const subscription = await lemonSqueezy.getSubscription(subscriptionId);
return {
status: subscription.attributes.status,
currentPeriodEnd: subscription.attributes.current_period_end,
cancelAtPeriodEnd: subscription.attributes.cancel_at_period_end,
};
}
export async function cancelSubscription(subscriptionId: string) {
return await lemonSqueezy.cancelSubscription(subscriptionId);
}
export async function reactivateSubscription(subscriptionId: string) {
return await lemonSqueezy.reactivateSubscription(subscriptionId);
}
Customer Portal
// src/components/customer-portal.tsx
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
interface CustomerPortalProps {
customerId: string;
}
export const CustomerPortal = ({ customerId }: CustomerPortalProps) => {
const [loading, setLoading] = useState(false);
const openPortal = async () => {
setLoading(true);
try {
const response = await fetch('/api/customer-portal', {
method: 'POST',
body: JSON.stringify({ customerId }),
});
const { url } = await response.json();
window.location.href = url;
} catch (error) {
console.error('Portal error:', error);
} finally {
setLoading(false);
}
};
return (
<Button onClick={openPortal} disabled={loading}>
{loading ? 'Loading...' : 'Manage Subscription'}
</Button>
);
};
Webhook Handling
Webhook Configuration
// src/app/api/webhooks/lemon-squeezy/route.ts
import { NextResponse } from 'next/server';
import { headers } from 'next/headers';
import { env } from '@/env';
import { db } from '@/server/db';
import { WebhookEvent } from '@lemonsqueezy/lemonsqueezy.js';
import { createHmac } from 'crypto';
export async function POST(req: Request) {
const body = await req.text();
const headersList = headers();
const signature = headersList.get('x-signature');
// Verify webhook signature
const hmac = createHmac('sha256', env.LEMON_SQUEEZY_WEBHOOK_SECRET);
const digest = hmac.update(body).digest('hex');
if (signature !== digest) {
return new NextResponse('Invalid signature', { status: 401 });
}
const event = JSON.parse(body) as WebhookEvent;
try {
await handleWebhookEvent(event);
return new NextResponse('OK', { status: 200 });
} catch (error) {
console.error('Webhook error:', error);
return new NextResponse('Internal error', { status: 500 });
}
}
async function handleWebhookEvent(event: WebhookEvent) {
switch (event.meta.event_name) {
case 'subscription_created':
await handleSubscriptionCreated(event);
break;
case 'subscription_updated':
await handleSubscriptionUpdated(event);
break;
case 'subscription_cancelled':
await handleSubscriptionCancelled(event);
break;
case 'order_created':
await handleOrderCreated(event);
break;
// Handle other events...
}
}
Event Handlers
// src/lib/webhook-handlers.ts
import { WebhookEvent } from '@lemonsqueezy/lemonsqueezy.js';
import { db } from '@/server/db';
import { subscriptions, orders } from '@/server/db/schema';
export async function handleSubscriptionCreated(event: WebhookEvent) {
const { data } = event;
await db.insert(subscriptions).values({
id: data.id,
customerId: data.attributes.customer_id,
status: data.attributes.status,
productId: data.attributes.product_id,
variantId: data.attributes.variant_id,
currentPeriodEnd: new Date(data.attributes.current_period_end),
cancelAtPeriodEnd: data.attributes.cancel_at_period_end,
});
}
export async function handleOrderCreated(event: WebhookEvent) {
const { data } = event;
await db.insert(orders).values({
id: data.id,
customerId: data.attributes.customer_id,
status: data.attributes.status,
total: data.attributes.total,
currency: data.attributes.currency,
orderNumber: data.attributes.order_number,
});
// Handle digital delivery
if (data.attributes.status === 'paid') {
await deliverDigitalProduct(data);
}
}
License Verification
License Key Validation
// src/lib/license.ts
import { lemonSqueezy } from '@/lib/lemon-squeezy';
export async function validateLicenseKey(key: string) {
try {
const license = await lemonSqueezy.getLicenseKey(key);
return {
valid: license.attributes.status === 'active',
activation_limit: license.attributes.activation_limit,
activation_usage: license.attributes.activation_usage,
expires_at: license.attributes.expires_at,
};
} catch (error) {
return { valid: false };
}
}
export async function activateLicense(key: string, instanceId: string) {
return await lemonSqueezy.activateLicenseKey(key, {
instance_name: instanceId,
});
}
Usage Tracking
Metered Billing
// src/lib/usage.ts
import { lemonSqueezy } from '@/lib/lemon-squeezy';
export async function reportUsage(
subscriptionId: string,
quantity: number
) {
return await lemonSqueezy.createSubscriptionUsage(subscriptionId, {
quantity,
});
}
export async function getUsage(subscriptionId: string) {
const usage = await lemonSqueezy.getSubscriptionUsage(subscriptionId);
return {
current: usage.attributes.current_usage,
period_start: usage.attributes.period_start,
period_end: usage.attributes.period_end,
};
}
Best Practices
Security
Validate webhook signatures
Use environment variables for keys
Implement proper error handling
Database
Store subscription data
Track order history
Maintain customer records
User Experience
Handle loading states
Provide clear error messages
Implement proper redirects
Testing
Use test mode for development
Validate webhook handling
Test subscription flows
Next Steps