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 single axis that organizes this decision is control-versus-burden: the more a provider does for you, the less you can shape it. Stripe, Paddle, and a custom engine sit at three points on that spectrum, and each point trades a different amount of liability for a different amount of freedom.

Billing build-vs-buy spectrum Paddle carries the most burden and offers the least control; a custom engine offers total control at maximum burden; Stripe Billing sits between. less control · less burden more control · more burden Paddle (MoR) tax, PCI, disputes all Paddle's ~5% all-in Stripe Billing you remit tax own the FSM ~2.9% + Billing/Tax Custom engine everything yours platform team interchange + infra
One spectrum organizes the choice: each step right buys control and costs liability, dev effort, and operational burden.

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.

Modeling the real crossover point

The fee table hides the number that actually decides build-versus-buy: the revenue at which a custom engine’s fixed cost undercuts a platform’s percentage. Take a SaaS doing $6,000,000 in annual card volume with a 1.4% combined dispute-and-refund rate. On Stripe Billing you pay roughly 2.9% + 30¢ per charge, another ~0.5% for Stripe Billing itself, and ~0.5% for Stripe Tax — call it 4.0% all-in, or about $240,000 a year. Paddle at a blended 5% is closer to $300,000, but that figure already absorbs the 1.4% you would otherwise eat in disputes plus the $15,000–$30,000 tax-filing vendor you would need standalone. A custom engine on interchange-plus pricing might land near 2.7%, or $162,000 — except a two-engineer billing team, fully loaded, costs north of $400,000 before you add the tax-engine subscription, account-updater fees, and an on-call rotation. At $6M the platform wins outright; the custom engine only breaks even somewhere past $40–60M in volume, and only if headcount stays flat. Run this arithmetic with your own dispute rate and average invoice_id amount before anyone drafts a scheduler design doc.

Note that the tax row is the one teams misjudge most. Under Stripe Billing you are the merchant of record, so the moment you cross an economic-nexus threshold — $100,000 in sales or 200 transactions in many US states, or the first euro of B2C sales into most EU countries — the filing obligation is yours. Stripe Tax computes the correct rate per line item and retains the evidence, but the returns, the registrations, and the audit exposure sit on your legal entity. Paddle removes that entirely by being the seller named on the invoice: the end customer’s contract is with Paddle, the VAT number on the receipt is Paddle’s, and a French tax authority’s questions land in Paddle’s inbox, not yours.

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. Notice how much your code owns shrinks as you move toward the Merchant of Record.

Ownership by integration With Stripe you own the FSM and remit tax; with Paddle you only mirror webhooks; with a custom engine you own scheduling, retries, and tax end to end. Stripe Billing you own: FSM, tax remit Stripe: checkout, calc webhook = source of truth Paddle (MoR) you own: mirror + provision Paddle: everything else react to webhooks only Custom engine you own: scheduler, retries tax, disputes, SCA PSP charges only
Your code surface shrinks from a full engine (custom) to a webhook mirror (Paddle) as the provider absorbs more.

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

Why the webhook, not the API response, is the source of truth

In all three integrations the synchronous API response is a hint, not a fact. A subscriptions.create call can return status: 'incomplete' while the first payment is still confirming through 3-D Secure, and the authoritative transition to active arrives seconds later on a customer.subscription.updated webhook. If you provision entitlements off the create response, you will occasionally grant access to a subscription whose payment never completes and then have to claw it back. The disciplined pattern is identical across Stripe, Paddle, and a custom engine: persist a local row keyed by subscription_id in a pending state, then let the webhook handler — made idempotent with a stored idempotency_key or the provider’s event id — drive every transition. Deduplicate on that event id because every provider redelivers at least once, and funnel events through a single-writer path per subscription_id so a delayed deleted cannot overwrite a newer active. This is also the discipline that keeps the provider replaceable: your FSM, not their API shape, is what the rest of your product reads.

Verification & Testing

Whatever you choose, the invariant under test is the same: your internal view of a subscription must never drift from the provider’s. The test loop below replays provider events and asserts your FSM lands where the provider says it should, with a nightly reconciliation as the backstop.

Provider reconciliation loop Fire provider test events, assert the internal FSM matches the provider state, then reconcile subscription count and MRR nightly. Fire test events stripe trigger / sandbox Assert FSM match idempotent on replay Nightly reconcile count + MRR vs provider
The same reconciliation loop applies to every option — only the size of the surface it must cover changes.

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.

One test catches more production incidents than any unit assertion: the deliberate replay of a stale event. Capture a real customer.subscription.updated payload, apply it to advance a subscription_id to active, then apply an older payload for the same subscription and assert your FSM refuses to regress. Providers do not guarantee ordering, and Paddle in particular batches webhook retries, so a naive last-writer-wins handler will silently downgrade an active customer to past_due when a delayed retry lands minutes after the newer event. Version every stored subscription row with the provider’s sequence number or occurred_at timestamp and drop any inbound event older than the row you already hold. Pair that with a chaos test that redelivers the same event id five times and asserts exactly one entitlement grant and one ledger row — the two invariants a duplicate must never break.

Gotchas & Production Pitfalls

The pitfalls below cluster around three misconceptions: that a Merchant of Record migration is a code change, that “calculates tax” means “handles tax”, and that charging a card is the hard part of a custom engine. The map names each and its correction.

Platform choice pitfalls MoR migration is a customer-data event, Stripe Tax calculates but does not remit, custom engines underestimate dunning, and fee comparisons must be all-in. MoR migration not a code change re-collect cards decide early Tax calc ≠ file Stripe computes you still remit register per nexus Dunning is hard charging is easy retries, SCA, updater budget for it Fees all-in not headline rate add tax, disputes model your rates
Four pitfalls, three misconceptions — each correction is one sentence, and each ignored one is a quarter of wasted work.
  • 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_tax does 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.
  • Proration math differs per provider and leaks into your revenue reports. Stripe prorates by the second against the invoice anchor and emits explicit credit line items on the next invoice_id; Paddle applies its own rounding and you only ever see the net figure. If you compute MRR from your own FSM, you must replicate the provider’s proration to the cent, or your dashboard will disagree with the money that actually moved and every board deck becomes an argument. Reconcile on integer minor units, not on your model’s opinion of them.
  • Refunds and disputes flow through different objects than charges. A chargeback under Stripe arrives as a charge.dispute.created event and can land 60+ days after the original renewal, reversing revenue you already recognized; under Paddle you never see it because Paddle ate the liability. If your custom engine treats a refund as merely a negative charge, your tax remittance will over-report, because the VAT on a refunded invoice must be reclaimed in the same jurisdiction you paid it. Model reversals as first-class ledger entries tied back to the original subscription_id, not as arithmetic on the running total.

Frequently Asked Questions

When does a merchant-of-record model make sense? When tax registration and compliance across many jurisdictions would otherwise dominate engineering time, and the higher effective rate is worth avoiding it. It becomes less attractive as volume grows and the percentage cost exceeds the compliance cost it replaces.

What is the strongest argument for building? A pricing model the platforms cannot express — drawdown commitments, complex ramps, or unusual metering. Building to save fees rarely pays back once the ongoing maintenance is counted.

Can the decision be deferred? Partly, by keeping your own subscription state authoritative and the provider as an execution engine. That preserves the option to change without a rewrite, which is worth more than getting the initial choice right.

How much does switching later cost? Considerably more than the initial integration, dominated by untangling provider objects from application code rather than by moving data. A provider-neutral seam is what keeps the cost bounded.