Payments
Payment processing configuration using Lemon Squeezy in ShipKit
Payments
ShipKit uses Lemon Squeezy for payment processing and subscription management.
Features
- One-time payments
- Subscriptions
- Usage-based billing
- Checkout pages
- Webhooks
- Analytics
Configuration
Add these environment variables to enable payments:
# Required
LEMONSQUEEZY_API_KEY=your_api_key
LEMONSQUEEZY_STORE_ID=your_store_id
LEMONSQUEEZY_WEBHOOK_SECRET=your_webhook_secret
# Feature Flag
NEXT_PUBLIC_FEATURE_LEMONSQUEEZY_ENABLED=true
Get your credentials from the Lemon Squeezy Dashboard.
Setup Steps
- Create a Lemon Squeezy account
- Set up your store
- Create products/variants
- Configure webhooks (see security guide below)
- Set up API keys
Products vs Variants
Important: Lemon Squeezy uses a hierarchical structure:
- Products are the main items (e.g., "Shipkit Pro")
- Variants are specific versions of products with pricing (e.g., "Shipkit Pro - Monthly", "Shipkit Pro - Yearly")
For checkout and payment verification, you must use Variant IDs, not Product IDs.
Products Configuration
Configure your product variants in config/site-config.ts
:
store: {
products: {
// Use Variant IDs (not Product IDs) for checkout URLs
// Get these from your Lemon Squeezy dashboard
basic: "eb159dba-96a3-40f2-a97b-7b9117e635a1", // variant ID
pro: "4d259175-0a79-486a-b0f8-b77404ee68df", // variant ID
enterprise: "7935a386-7cd0-47fe-83c8-cab101323591", // variant ID
shipkit: "20b5b59e-b4c4-43b0-9979-545f90c76f28" // variant ID
}
}
How to Find Variant IDs
- Go to your Lemon Squeezy Dashboard
- Click on a product
- Click on a variant (pricing option)
- Copy the UUID from the URL:
/variants/{variant-id}
Payment Verification
Our payment system correctly handles variant-based verification:
// Check if user purchased a specific variant
await checkUserPurchasedVariant(variantId);
// Check if user purchased any configured product
await hasUserPurchasedAnyConfiguredProduct(userId);
// Get all purchased products for a user
await getUserPurchasedConfiguredProducts(userId);
Checkout Flow
The checkout flow is now properly configured:
- BuyButton uses
routes.external.buy
- Site config generates URLs with correct variant IDs
- LemonSqueezy Pricing displays only configured variants
- Payment verification checks against variant IDs
Webhooks Security & Best Practices
Security Requirements
CRITICAL: Always verify webhook signatures to prevent malicious actors from sending fake webhooks:
function verifyWebhookSignature(rawBody: string, signature: string): boolean {
const hmac = crypto.createHmac("sha256", env.LEMONSQUEEZY_WEBHOOK_SECRET);
const digest = Buffer.from(hmac.update(rawBody, "utf8").digest("hex"), "hex");
const signatureBuffer = Buffer.from(signature, "hex");
// Use timing-safe comparison
return crypto.timingSafeEqual(digest, signatureBuffer);
}
Webhook Configuration
- Set webhook URL in Lemon Squeezy dashboard:
https://yourdomain.com/api/webhooks/lemonsqueezy
- Generate a secure webhook secret
- Subscribe to required events:
order_created
- New purchasesorder_refunded
- Handle refundssubscription_created
- New subscriptionssubscription_updated
- Plan changessubscription_cancelled
- Cancellationssubscription_resumed
- Reactivationssubscription_expired
- Expirationssubscription_payment_*
- Payment events
Implementation Best Practices
- Process asynchronously - Return 200 immediately, process in background
- Implement idempotency - Handle duplicate webhooks gracefully
- Log everything - Track all webhook events for debugging
- Handle all event types - Even if you don't process them yet
- Test thoroughly - Use Lemon Squeezy's webhook testing tools
Multiple Payment Providers
ShipKit also supports:
- Stripe - Enterprise-grade payments
- Polar - Creator-focused platform
Enable with feature flags:
NEXT_PUBLIC_FEATURE_PAYMENTS_STRIPE_ENABLED=true
NEXT_PUBLIC_FEATURE_PAYMENTS_POLAR_ENABLED=true
See provider-specific documentation in /docs/integrations/
⚠️ CRITICAL: Webhooks are essential for secure payment processing. Our implementation includes:
- ✅ Signature verification (prevents fake webhooks)
- ✅ Idempotency protection (prevents duplicate processing)
- ✅ Database transactions (ensures data consistency)
- ✅ Comprehensive event handling (orders, subscriptions, payments)
- ✅ Error handling & retries (reliable processing)
- ✅ Variant ID tracking (proper product verification)
Quick Security Check
Run through this checklist immediately:
- [ ]
LEMONSQUEEZY_WEBHOOK_SECRET
is set in your environment - [ ] Webhook signature verification is enabled (not commented out)
- [ ] HTTPS is used for webhook endpoints
- [ ] Database transactions are used for consistency
- [ ] Variant IDs are configured correctly in site config
Resources
- 📖 Complete Webhook Best Practices Guide - Comprehensive implementation guide
- 🔐 Security Audit Checklist - Quick security verification
Webhook Endpoint
Your webhook endpoint is available at:
https://yourdomain.com/webhooks/lemonsqueezy
Configure this URL in your Lemon Squeezy dashboard with the following events:
order_created
order_refunded
subscription_created
subscription_updated
subscription_cancelled
subscription_resumed
subscription_expired
subscription_payment_success
subscription_payment_failed
Testing
Local Development
# Install ngrok for local testing
npm install -g ngrok
# Start your dev server
npm run dev
# Expose localhost
ngrok http 3000
# Use ngrok URL in Lemon Squeezy webhook settings
# Example: https://abc123.ngrok.io/webhooks/lemonsqueezy
Test Mode
Always test with Lemon Squeezy test mode before going live:
- Enable test mode in your Lemon Squeezy dashboard
- Create test products and variants
- Use test credit card numbers
- Verify webhook events are processed correctly
- Confirm variant IDs match between test and production
Troubleshooting
Common Issues
"Checkout button not working"
- Verify variant IDs in
site-config.ts
are correct - Check that
NEXT_PUBLIC_FEATURE_LEMONSQUEEZY_ENABLED=true
- Ensure the buy URL is generating correctly
"Payment verification failing"
- Confirm webhook is processing
order_created
events - Check that variant IDs are being saved in payment metadata
- Verify database transactions are completing
"Products not displaying"
- Make sure variant IDs in config match actual variants in Lemon Squeezy
- Check API key permissions
- Verify feature flag is enabled
Debug Commands
// Check if user has paid
const hasPaid = await PaymentService.getUserPaymentStatus(userId);
// Check specific variant purchase
const hasPurchased = await checkUserPurchasedVariant(variantId);
// Get configured variant IDs
const variantIds = await getConfiguredVariantIds();