Payments
Stripe Checkout solves payments for almost every MVP. Here's the short list of when to pick something else, and the rules that keep you out of PCI hell.
Payments is one of those areas where the right answer is boring and the wrong answer is expensive. For 90% of MVPs, the right answer is Stripe Checkout — hosted, six lines of code, no card data ever touches your server. This page covers the alternatives, the compliance trap that catches first-time builders, and the webhook discipline that prevents your worst day.
Stripe Checkout
The default. You hand Stripe a list of line items, they hand back a URL, you redirect the user to it. They take the card, they charge it, they redirect back. You never see the card number.
What you get: Apple Pay, Google Pay, Link, Klarna, Afterpay, SEPA, iDEAL, BECS, ACH — all just on. Subscription billing. Tax (with the Stripe Tax addon). Receipts. A passable customer portal. Refunds via the dashboard. A test mode that's actually usable.
What it costs: 2.9% + $0.30 per successful card charge in the US. International cards are +1.5%. Currency conversion is +1%. ACH is 0.8% capped at $5.
What it doesn't get you: a fully embedded checkout experience (use Elements for that), and Merchant of Record status (Stripe is your processor, not your seller of record — you're still on the hook for sales tax and VAT registration unless you bolt on Stripe Tax).
Stripe Elements
When you genuinely need the checkout to live inside your page — a multi-step onboarding where billing is one of several screens, or a UI where the page chrome matters — use Elements instead of Checkout. You drop a Stripe-hosted iframe into your form and collect the card on your domain.
This is more work. You're managing form state, error handling, 3D Secure flows, and the Payment Element configuration. Don't reach for Elements because Checkout looks "too Stripe-y." Reach for it when you've shipped Checkout, watched conversion, and have a real reason to integrate deeper.
Stripe Connect
If you're building a marketplace — Uber for X, Airbnb for Y, anywhere money flows from a customer through you to a third party — you need Connect.
Two flavors that matter:
- Standard. The connected account is a real Stripe account owned by your seller. They onboard themselves, get their own dashboard, manage their own disputes. Light integration. You don't see most of their data.
- Express. Stripe hosts a stripped-down dashboard for your sellers. You handle more of the UX. Better for "we have sellers but they aren't sophisticated businesses."
Custom (the third flavor) hides Stripe entirely from your sellers but pushes all compliance work onto you, including identity verification UI. Don't pick Custom unless you've already shipped a marketplace.
US-side gotcha: if you're a marketplace paying out more than $600/year to a US contractor, you owe them a 1099. Stripe Connect can file these for you (the 1099-K is automatic, the 1099-NEC requires the Tax addon). Don't skip this.
Paddle
Paddle is a Merchant of Record. That means Paddle is the seller; you're effectively wholesaling to Paddle who then resells to your customer. The practical effect: Paddle handles VAT registration in every EU country, US sales tax in every state, GST in Australia and India, the works. You see one payout, one tax form (1099-K from Paddle if you're US-based).
This matters because EU VAT rules require you to register and remit in any country you sell digital goods to, with no minimum threshold. Solo founders selling globally get crushed by this. Paddle makes it go away.
What you give up: Paddle's fees are higher (5% + $0.50 typical) and the product is less flexible than Stripe. Custom checkout flows are limited. Subscription metadata is clunkier. The dashboard is fine but not great.
Polar and Lemon Squeezy
Polar is a newer, OSS-aligned MoR built initially for funding open-source maintainers. Currently around 4% + $0.40, includes tax, integrates with GitHub Sponsors. Good fit if you're a developer selling to developers.
Lemon Squeezy was acquired by Stripe in 2024 but still operates as a separate MoR product. Same value prop as Paddle (taxes handled), often a friendlier dashboard, similar fee structure. The Stripe acquisition means it's getting better integration with Stripe primitives over time, but for now it's still its own product.
Apple and Google IAP
If you ship a native mobile app and you're selling digital goods consumed in the app, you generally have to use Apple's StoreKit on iOS and Google Play Billing on Android. The platforms take 15–30%.
Workarounds in 2026: Apple now permits external links to web checkout in many regions following the EU Digital Markets Act and various US court rulings, but you have to follow specific entitlement rules and a "scare screen" still appears. The DMA also opened up alternative app stores in the EU. None of this changes the basic answer: if your app is mobile-first and sells consumables or subscriptions consumed in-app, budget for the 15–30% and use IAP. If you have a serious web presence, route signups through web first.
Comparison
| Provider | Fees (typical) | Subscriptions | Merchant of Record | Embedded option | Free tier |
|---|---|---|---|---|---|
| Stripe Checkout | 2.9% + $0.30 | Yes | No (add Stripe Tax) | No (Elements is separate) | No, but no monthly fee |
| Stripe Elements | 2.9% + $0.30 | Yes | No | Yes | Same |
| Stripe Connect | 2.9% + $0.30 + Connect fees | Yes | No | Both | Same |
| Paddle | ~5% + $0.50 | Yes | Yes | Limited | No monthly fee |
| Polar | ~4% + $0.40 | Yes | Yes | Yes | No monthly fee |
| Lemon Squeezy | 5% + $0.50 | Yes | Yes | Yes | No monthly fee |
| Apple IAP | 15–30% | Yes | Yes | N/A (native only) | No |
| Google Play Billing | 15–30% | Yes | Yes | N/A (native only) | No |
The PCI-DSS thing
This is the part that catches people. The card industry classifies merchants by how much card data they touch:
- SAQ A — you never see card data. The card is collected by a PCI-compliant third party (Stripe Checkout, Paddle, etc.) that you redirect to. You fill out a 22-question self-assessment annually and you're done.
- SAQ A-EP — you have a payment page on your domain that loads scripts from a PCI-compliant provider. Stripe Elements falls here. Slightly more questions, still self-assessed.
- SAQ D — you handle card numbers in any form, even briefly. Hundreds of controls. External auditor required if you process more than 6 million transactions/year, but the controls apply at any volume.
Stripe Checkout is SAQ A. Rolling your own card form (even one that POSTs straight to Stripe's API) is SAQ D. The gap is not subtle. The only reason to roll your own card form is if you've raised a Series B and have a compliance team. For an MVP, do not type the words "card number" into your codebase.
Long version: /blog/stripe-checkout-pci/.
One-time vs subscription
Both work in Checkout. Subscriptions add a few wrinkles:
- Use Stripe's hosted Customer Portal for plan changes and cancellations. Building your own is a tarpit.
- Set
subscription_data.trial_period_daysfor trials. Don't manage trials in your own DB. - Handle proration on plan changes by sending
proration_behavior: 'create_prorations'. The default surprises people. - Failed payments retry on Stripe's smart retry schedule. Don't write your own retry loop.
The webhook-is-authoritative rule
The single most-broken pattern in payments code: granting access on the success-page redirect.
// WRONG: user lands on /success after Checkout
app.get('/success', (req, res) => {
grantProAccess(req.user); // anyone can hit this URL!
});
The success URL is just a URL. Users can navigate to it directly, bookmark it, share it. You must grant access from the webhook that Stripe sends you, after verifying the signature:
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
app.post(
"/webhooks/stripe",
express.raw({ type: "application/json" }),
(req, res) => {
const sig = req.headers["stripe-signature"]!;
const event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET!,
);
if (event.type === "checkout.session.completed") {
const session = event.data.object;
grantProAccess(session.client_reference_id!);
}
res.json({ received: true });
},
);
The success page should say "thanks, your account is being upgraded — refresh in a moment." Then the webhook does the actual work. Stripe retries failed webhooks for up to three days, which is exactly what you want.
Tax
Two roads:
- Stay with Stripe. Add Stripe Tax (0.5% per transaction). It calculates correctly and files in many US states. EU VAT registration is still on you unless you stay under thresholds.
- Use a Merchant of Record (Paddle, Polar, Lemon Squeezy). Higher per-transaction fee, no tax operations.
If you're solo, US-based, and selling globally to consumers, the MoR math usually wins. If you're selling to businesses (B2B SaaS), Stripe Tax is fine because most B2B sales are reverse-charged anyway.
Anti-patterns
- Rolling your own card form. SAQ D nightmare. Don't.
- Storing card numbers anywhere. Including logs. Including "just for debugging."
- Granting access on the success page. Webhook or nothing.
- Hardcoding price IDs. Put them in env vars. You will create new prices and want to swap without a deploy.
- Skipping signature verification on webhooks. A forged webhook can grant a stranger Pro access for free.
- Letting trials extend forever. Always set
trial_period_days. - Ignoring
payment_intent.payment_failed. Subscriptions die quietly otherwise.
Our recommendation
Stripe Checkout for almost every MVP. Add Stripe Tax when you need it. Wire the webhook before you wire the success page. This is what this site uses for its premium tier and there's no plausible reason to use anything else for a v1.
The exception: if you're a solo founder selling digital products globally and the thought of registering for VAT in 27 EU countries makes you want to quit, use Paddle instead. The 2% extra in fees is the cheapest insurance you'll ever buy.
If you're building a marketplace, Stripe Connect Standard. If you're building a mobile-first product selling consumables, IAP and budget for the 30%.