
Payment Integration Guide: Stripe, LemonSqueezy, PayPal & More
Payment Integration Guide: Multiple Payment Providers
Accepting payments is critical for any SaaS business. Our boilerplate supports four major payment providers out of the box, giving you flexibility to choose what works best for your market and customers.
Supported Payment Providers
- Stripe: Most popular, excellent API, global coverage
- LemonSqueezy: Merchant of record, handles taxes and compliance
- PayPal: Trusted brand, especially strong in certain regions
- DodoPayments: Emerging provider with competitive rates
Why Multiple Providers?
Different providers excel in different scenarios:
Use Stripe if:
- You want the best developer experience
- You need advanced features (usage-based billing, invoicing)
- You're comfortable handling tax compliance
Use LemonSqueezy if:
- You want them to be the merchant of record
- You don't want to handle VAT/sales tax yourself
- You're a solo founder or small team
Use PayPal if:
- Your customers prefer PayPal
- You need strong coverage in specific regions
- You want a trusted, recognizable checkout
Use DodoPayments if:
- You want lower fees
- You need specific regional support
- You're experimenting with newer providers
Configuration
Environment Variables
Each provider requires API keys in your .env file:
# Stripe
STRIPE_SECRET_KEY=sk_test_51...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
# LemonSqueezy
LEMON_SQUEEZY_API_KEY=...
LEMON_SQUEEZY_STORE_ID=...
LEMON_SQUEEZY_WEBHOOK_SECRET=...
# PayPal
NEXT_PUBLIC_PAYPAL_CLIENT_ID=...
PAYPAL_SECRET_KEY=...
PAYPAL_WEBHOOK_ID=...
# DodoPayments
DODO_API_KEY=...
DODO_WEBHOOK_SECRET=...
Database Schema
Our organization model stores billing data for all providers:
export const organizations = pgTable("organizations", {
id: uuid("id").primaryKey().defaultRandom(),
name: text("name").notNull(),
// Universal fields
planId: text("plan_id").references(() => plans.id),
subscriptionStatus: text("subscription_status"),
currentPeriodEnd: timestamp("current_period_end"),
// Stripe
stripeCustomerId: text("stripe_customer_id"),
stripeSubscriptionId: text("stripe_subscription_id"),
// LemonSqueezy
lemonSqueezyCustomerId: text("lemon_squeezy_customer_id"),
lemonSqueezySubscriptionId: text("lemon_squeezy_subscription_id"),
// PayPal
paypalSubscriptionId: text("paypal_subscription_id"),
paypalCustomerId: text("paypal_customer_id"),
// DodoPayments
dodoCustomerId: text("dodo_customer_id"),
dodoSubscriptionId: text("dodo_subscription_id"),
});
Stripe Integration
Creating a Subscription
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
// Create customer
const customer = await stripe.customers.create({
email: user.email,
name: organization.name,
metadata: {
organizationId: organization.id,
},
});
// Create subscription
const subscription = await stripe.subscriptions.create({
customer: customer.id,
items: [{ price: 'price_1234...' }],
payment_behavior: 'default_incomplete',
expand: ['latest_invoice.payment_intent'],
});
// Save to database
await db.update(organizations)
.set({
stripeCustomerId: customer.id,
stripeSubscriptionId: subscription.id,
subscriptionStatus: subscription.status,
})
.where(eq(organizations.id, organization.id));
Handling Webhooks
Stripe sends webhooks for subscription events:
// app/api/webhooks/stripe/route.ts
export async function POST(req: Request) {
const body = await req.text();
const sig = req.headers.get('stripe-signature');
const event = stripe.webhooks.constructEvent(
body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
switch (event.type) {
case 'customer.subscription.updated':
await handleSubscriptionUpdate(event.data.object);
break;
case 'customer.subscription.deleted':
await handleSubscriptionCancelled(event.data.object);
break;
case 'invoice.payment_succeeded':
await handlePaymentSucceeded(event.data.object);
break;
case 'invoice.payment_failed':
await handlePaymentFailed(event.data.object);
break;
}
return new Response('OK', { status: 200 });
}
Customer Portal
Give customers a way to manage their subscription:
// Generate portal session
const session = await stripe.billingPortal.sessions.create({
customer: organization.stripeCustomerId,
return_url: `${process.env.APP_ORIGIN}/settings/billing`,
});
// Redirect user
redirect(session.url);
LemonSqueezy Integration
LemonSqueezy is particularly popular for digital products because they handle:
- VAT/sales tax collection
- EU VAT MOSS compliance
- Invoicing and receipts
- Fraud prevention
Creating a Checkout
import { lemonSqueezySetup, createCheckout } from '@lemonsqueezy/lemonsqueezy.js';
lemonSqueezySetup({ apiKey: process.env.LEMON_SQUEEZY_API_KEY });
const checkout = await createCheckout({
storeId: process.env.LEMON_SQUEEZY_STORE_ID,
variantId: plan.lemonSqueezyVariantId,
checkoutData: {
email: user.email,
name: organization.name,
custom: {
organization_id: organization.id,
},
},
});
// Redirect to checkout
redirect(checkout.data.attributes.url);
Webhook Handling
// app/api/webhooks/lemonsqueezy/route.ts
export async function POST(req: Request) {
const body = await req.json();
// Verify signature
const signature = req.headers.get('x-signature');
if (!verifySignature(body, signature)) {
return new Response('Invalid signature', { status: 401 });
}
switch (body.meta.event_name) {
case 'subscription_created':
await handleSubscriptionCreated(body.data);
break;
case 'subscription_updated':
await handleSubscriptionUpdated(body.data);
break;
case 'subscription_cancelled':
await handleSubscriptionCancelled(body.data);
break;
}
return new Response('OK', { status: 200 });
}
Plans and Pricing
Define plans in your database:
export const plans = pgTable("plans", {
id: text("id").primaryKey(),
name: text("name").notNull(),
description: text("description"),
// Pricing
monthlyPrice: integer("monthly_price").notNull(),
yearlyPrice: integer("yearly_price"),
// Provider IDs
stripePriceId: text("stripe_price_id"),
lemonSqueezyVariantId: text("lemon_squeezy_variant_id"),
paypalPlanId: text("paypal_plan_id"),
dodoPlanId: text("dodo_plan_id"),
// Quotas
maxUsers: integer("max_users").default(-1),
maxProjects: integer("max_projects").default(-1),
maxStorageGB: integer("max_storage_gb").default(10),
// Features
features: json("features").$type<string[]>(),
});
Testing Payments
All providers offer test modes:
Stripe Test Cards:
- Success:
4242 4242 4242 4242 - Decline:
4000 0000 0000 0002 - 3D Secure:
4000 0027 6000 3184
LemonSqueezy:
- Use test mode in dashboard
- Test card:
4242 4242 4242 4242
PayPal:
- Create sandbox accounts at developer.paypal.com
- Use sandbox credentials for testing
Best Practices
1. Handle Webhooks Idempotently
Webhooks may be delivered multiple times:
async function handleSubscriptionUpdate(subscription: Stripe.Subscription) {
const existing = await db.query.organizations.findFirst({
where: eq(organizations.stripeSubscriptionId, subscription.id),
});
if (existing.subscriptionStatus === subscription.status) {
// Already processed
return;
}
// Update status
await db.update(organizations)
.set({ subscriptionStatus: subscription.status })
.where(eq(organizations.id, existing.id));
}
2. Log Everything
Create audit trails for billing events:
await createAuditLog({
organizationId,
action: 'subscription.updated',
metadata: {
oldStatus: 'active',
newStatus: 'cancelled',
provider: 'stripe',
},
});
3. Graceful Degradation
If a payment fails, don't immediately lock users out:
if (organization.subscriptionStatus === 'past_due') {
// Grace period: 3 days
const daysSinceFailed = differenceInDays(
new Date(),
organization.currentPeriodEnd
);
if (daysSinceFailed < 3) {
// Show warning banner
return { access: 'limited', warning: true };
}
// After grace period
return { access: 'restricted' };
}
4. Customer Communication
Send emails for important billing events:
await sendEmail({
to: organization.ownerEmail,
subject: 'Payment Failed',
template: 'payment-failed',
data: {
organizationName: organization.name,
amount: invoice.amount_due,
retryDate: invoice.next_payment_attempt,
},
});
Get Started
Our boilerplate includes:
- Pre-configured webhook endpoints for all providers
- Database schema for multi-provider support
- Example checkout flows
- Subscription management UI
- Test mode helpers
Clone and start accepting payments today:
git clone https://github.com/yourusername/b2b-boilerplate.git
cd b2b-boilerplate
pnpm install
Choose your provider, add your API keys, and you're ready to charge customers!