Stripe Billing vs Paddle vs Custom Engine
This decision lands on your desk at a specific moment: you are about to commit a billing data model and you need to know who owns tax, chargebacks, and the subscription state machine before you write the first migration. The three credible answers — Stripe Billing, Paddle as a Merchant of Record (MoR), and a custom engine on raw payment rails — differ less in features than in where liability and control sit. This page sits under Subscription Lifecycle States, because the choice you make here determines whether you own the state machine or merely mirror a provider’s. The wrong call is expensive: migrating off a Merchant of Record means re-collecting every customer’s payment method, and building a custom engine you don’t need can burn a quarter of engineering time on solved problems.
Trade-offs
The axes that actually drive the decision are tax liability, total cost as a percentage of revenue, vendor lock-in, dunning sophistication, compliance scope you carry, and engineering effort to build and operate. “Control” is the meta-axis: the more a provider does for you, the less you can customize.
| Dimension | Stripe Billing | Paddle (Merchant of Record) | Custom engine (on PSP rails) |
|---|---|---|---|
| Tax handling | You are the seller of record; Stripe Tax calculates VAT/GST/sales tax but you remit and file | Paddle is the seller of record; it calculates, collects, remits, and files globally | You build or integrate a tax engine and remit/file everywhere yourself |
| Effective fees | ~2.9% + 30¢ card + ~0.5–0.8% Billing + ~0.5% Tax | ~5% + 50¢ all-in (tax, fraud, MoR included) | PSP interchange-plus (~2.6–2.9%) + your infra and engineering cost |
| Vendor lock-in | Medium — card data is portable on request; subscription logic is Stripe-shaped | High — customers belong to Paddle; switching means re-collecting payment methods | Low — you own tokens (via your PSP) and all logic |
| Dunning / retries | Strong: Smart Retries, configurable schedules, hosted recovery emails | Strong: handled end-to-end by Paddle, little to configure | You build retry timing, decline-code routing, and dunning emails |
| Compliance scope you carry | PCI SAQ-A (hosted fields); tax registration in each jurisdiction; SCA handled by Stripe | Minimal: Paddle carries PCI, tax registration, SCA, and chargebacks | Full: PCI scope per your integration, SCA/3DS routing, tax, disputes |
| Chargeback / dispute liability | Yours (Stripe provides tooling) | Paddle’s — they absorb fraud and dispute liability | Yours, end to end |
| Dev effort to launch | Days to weeks | Days | Months; ongoing platform team |
| Control / customization | High over logic, bounded by Stripe’s model | Low — you accept Paddle’s checkout and rules | Total |
| Best fit | SaaS selling primarily B2B/US, or wanting control without owning rails | Small teams selling globally to consumers who want zero tax/compliance burden | Scale where fee savings exceed a platform team’s cost, or unusual billing models |
A blunt heuristic: if remitting VAT in 40 jurisdictions sounds like a job you do not want, Paddle’s extra ~2% is cheap insurance. If you are a US-centric B2B SaaS and want to own dunning and the data model, Stripe Billing is the default. Only consider a custom engine once your card volume is large enough that shaving fees pays for a dedicated billing team, or your pricing model genuinely does not fit either platform.
Step-by-Step Implementation
The integration shape differs sharply by choice. Below is the minimal “create a subscription” path for each, so the divergence is concrete.
1. Stripe Billing — you create and own the subscription
You hold a customer_id and price_id and call Stripe; your webhook handler is the source of truth for subscription lifecycle states.
const subscription = await stripe.subscriptions.create({
customer: customer_id,
items: [{ price: price_id }],
payment_behavior: 'default_incomplete',
// ✅ tax computed by Stripe, but YOU remit it
automatic_tax: { enabled: true },
expand: ['latest_invoice.payment_intent'],
});
// Source of truth still arrives via webhook → drive your own FSM
2. Paddle — the MoR owns the customer and the tax
You hand Paddle a price and a customer email; Paddle runs checkout, tax, and dunning. Your job is to react to its webhooks and provision entitlements.
// Server-side: create a transaction; Paddle hosts checkout + tax + MoR
const txn = await paddle.transactions.create({
items: [{ priceId: paddle_price_id, quantity: 1 }],
customerId: paddle_customer_id,
// ⚠️ no automatic_tax flag — tax is Paddle's responsibility entirely
});
// You only mirror state from `subscription.activated` / `subscription.canceled` webhooks
3. Custom engine — you orchestrate the PSP directly
You vault a payment method through your PSP, then run renewals from your own scheduler. You now own everything the platforms abstracted.
// Renewal tick: charge the vaulted token off-session, you own retries + tax
const intent = await psp.charges.create({
amount: amount_cents, // integer minor units, never float
currency: 'usd',
payment_method: payment_method_id,
off_session: true,
confirm: true,
idempotency_key: `renew:${subscription_id}:${period_start}`, // ✅ exactly-once
});
// On decline you must run your own dunning schedule and tax remittance pipeline
Verification & Testing
For Stripe and Paddle, the thing under test is your webhook reconciliation: replay each provider’s test events and assert your internal FSM lands in the same state the provider reports, with no drift after redelivery. Use the provider CLI (stripe trigger, Paddle’s sandbox simulations) to fire subscription.updated, payment failures, and cancellations, and assert idempotent handling. For a custom engine, you additionally test renewal scheduling against a mock clock, decline-code routing, and tax calculation across jurisdictions — a much larger surface. A useful cross-check for any choice: a reconciliation query that compares your active-subscription count and MRR against the provider’s reported figures nightly and alerts on divergence.
Gotchas & Production Pitfalls
- Migrating off a Merchant of Record is a customer-data event, not a code change. Paddle owns the cardholder relationship; you cannot export raw tokens. Switching means re-collecting payment methods from every active customer, with the churn that implies. Decide before you scale, not after.
- Stripe Tax calculates but does not absolve. Enabling
automatic_taxdoes not register you in any jurisdiction or file returns. You still cross economic-nexus thresholds and must register and remit. Teams routinely conflate “Stripe computes my tax” with “Stripe handles my tax.” - Custom engines underestimate dunning, not charging. Taking a card is easy; recovering failed renewals with issuer-aware retry timing, SCA re-authentication, and account-updater integration is where the months go. Budget for it explicitly.
- Fee comparisons must be all-in. Paddle’s ~5% looks high next to Stripe’s ~2.9% until you add Stripe Billing, Stripe Tax, your tax-filing vendor, and chargeback losses. Model effective cost on your refund and dispute rates, not the headline card rate.
- Lock-in is in the data model, not the API. Even with portable card data, a year of Stripe-shaped subscription logic (proration behaviors, invoice items, anchor semantics) is real switching cost. Keep your own FSM as the source of truth from day one so the provider stays replaceable.