Blog
Practical notes on design, product, and shipping for startups.
Practical notes on design, product, and shipping for startups.
Stripe vs Paddle vs Lemon Squeezy for a SaaS selling worldwide in 2026: merchant of record vs processor, list-price fee tables, a worked $20 subscription example, when each one fits, how hard switching is, and provider-agnostic Next.js webhook code.
For a SaaS MVP selling worldwide, pick Stripe if you want the lowest fees and full control and can handle sales tax and VAT yourself. Pick Paddle or Lemon Squeezy if you want a merchant of record that collects and files those taxes for you at a flat 5% plus 50 cents per transaction. Decide tax liability first.
Last updated: November 3, 2026. Prices are list prices from each provider's public pricing page on that date. Check them again before you commit, because they change.
The big difference is not the checkout. It is who is legally selling to your customer.
Stripe also now offers its own merchant of record option, called Managed Payments, for digital products sold through Stripe Checkout or Payment Links. It covers sales tax, VAT, and GST in more than 80 countries, plus fraud, disputes, and transaction-level support, but it does not work with Stripe Connect, embedded payment components, or subscriptions created outside Checkout. Check its current fee and country list on Stripe's site before relying on it.
| Stripe (processor) | Paddle (MoR) | Lemon Squeezy (MoR) | |
|---|---|---|---|
| Base card fee | 2.9% + 30¢ (domestic US cards) | 5% + 50¢ per checkout transaction | 5% + 50¢ per transaction |
| Subscriptions | Stripe Billing, 0.7% of billing volume (pay as you go) | Included | Included |
| Tax calculation | Stripe Tax, 0.5% per transaction where you are registered (no-code integrations) | Included | Included |
| Tax registration and filing | You, an accountant, or Stripe Tax Complete (priced separately) | Included | Included |
| International cards and currency conversion | Extra fees apply | Included in the flat fee | "Some payments may be subject to additional fees" |
| Chargebacks | $15 per dispute received, plus fees to contest | Handled by Paddle | Handled by Lemon Squeezy |
| Buyer billing support | You | Paddle | Lemon Squeezy |
| Monthly fee | None on standard pricing | None | None |
Here is the plain arithmetic on one $20 per month subscription, using only the list prices above. This is an example calculation, not data from a Luminix Studio project.
| Provider | Fee on one $20 charge | Share of revenue | What it still leaves you to do |
|---|---|---|---|
| Stripe + Billing + Tax | $0.58 + $0.30 + $0.14 + $0.10 = $1.12 | about 5.6% | Register, file, and remit tax; handle disputes and billing emails |
| Paddle | $1.00 + $0.50 = $1.50 | 7.5% | Very little on the tax side |
| Lemon Squeezy | $1.00 + $0.50 = $1.50 | 7.5% before any extras | Very little on the tax side |
Two things this table shows. First, the fixed 30 or 50 cents hurts most on cheap plans, so a $5 plan loses a much bigger share than a $50 plan on every provider. Second, Stripe looks cheaper per transaction, but the gap is roughly two points of revenue, and the cost of tax registrations and filings in several countries can easily be more than that for a small team.
Choose Paddle, Lemon Squeezy, or Stripe Managed Payments when most of these are true:
A merchant of record is usually the faster path to a real launch for a small global SaaS. You trade a bit of margin for not having a tax project.
Choose Stripe as a processor when:
Harder than people expect, mainly because of saved cards. Card details live in the provider's vault, so moving active subscribers usually means a data migration that the old and new providers both have to support, or asking customers to re-enter their card. Plan for that cost when you choose.
You can make switching easier by keeping your app's billing logic provider-agnostic from day one:
subscriptions table and let webhooks update it. Your app checks that table, never the provider's API, to decide what a user can access.Here is a small, provider-agnostic schema for Postgres or Supabase.
create table subscriptions (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users(id),
provider text not null check (provider in ('stripe','paddle','lemonsqueezy')),
provider_customer_id text not null,
provider_subscription_id text not null,
plan text not null,
status text not null, -- active, trialing, past_due, canceled
current_period_end timestamptz,
updated_at timestamptz not null default now(),
unique (provider, provider_subscription_id)
);
create table billing_events (
provider text not null,
event_id text not null,
received_at timestamptz not null default now(),
primary key (provider, event_id) -- makes retries safe
);Each provider signs its webhooks differently. Always verify the signature against the raw request body before you trust the payload.
Stripe (App Router route handler, using the official SDK):
// app/api/webhooks/stripe/route.ts
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(req: Request) {
const body = await req.text(); // raw body, not req.json()
const signature = req.headers.get("stripe-signature") ?? "";
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
} catch {
return new Response("Invalid signature", { status: 400 });
}
if (await alreadyProcessed("stripe", event.id)) return new Response("ok");
if (event.type.startsWith("customer.subscription.")) {
const sub = event.data.object as Stripe.Subscription;
await upsertSubscription({
provider: "stripe",
providerSubscriptionId: sub.id,
providerCustomerId: String(sub.customer),
status: sub.status,
});
}
return new Response("ok");
}Paddle sends a Paddle-Signature header shaped like ts=...;h1=.... The h1 value is an HMAC-SHA256 of ts:rawBody using your webhook secret. Lemon Squeezy sends an X-Signature header, an HMAC-SHA256 hex digest of the raw body using your signing secret. A shared helper covers both:
// lib/billing/verify.ts
import crypto from "node:crypto";
function safeEqual(a: string, b: string) {
const ab = Buffer.from(a);
const bb = Buffer.from(b);
return ab.length === bb.length && crypto.timingSafeEqual(ab, bb);
}
export function verifyPaddle(rawBody: string, header: string, secret: string) {
const parts = Object.fromEntries(header.split(";").map((p) => p.split("=")));
if (!parts.ts || !parts.h1) return false;
const expected = crypto.createHmac("sha256", secret).update(`${parts.ts}:${rawBody}`).digest("hex");
return safeEqual(expected, parts.h1);
}
export function verifyLemonSqueezy(rawBody: string, header: string, secret: string) {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return safeEqual(expected, header);
}alreadyProcessed inserts into billing_events and returns true if the primary key already exists. upsertSubscription writes to your own table. Your pricing page, paywall, and feature flags then read only from subscriptions, so a provider change touches the webhook routes and checkout links, not the rest of the app.
Luminix Studio builds web and mobile apps, and the AI automations around them, for startups worldwide. When we scope an MVP that charges money, we settle the merchant of record question in the first week, build billing around your own subscriptions table with verified, idempotent webhooks, and keep the provider swappable. If you are choosing between Stripe, Paddle, and Lemon Squeezy for your product, or want billing built properly the first time, see luminixstudio.com.