Paddle Billing vs Stripe for EU-heavy SaaS: what merchant-of-record really costs
Paddle's merchant-of-record model sounds like a tax shortcut until you do the math on margins. Here's what I found building a SaaS with heavy EU traffic.
Paddle's merchant-of-record pitch sounds almost too good: they handle VAT, GST, sales tax, chargebacks, and compliance, and you just collect money. I believed it until I actually modeled out what a SaaS with 60% EU revenue looks like under both Paddle and Stripe. The numbers were not what I expected, and the tradeoffs are more nuanced than either company's marketing admits.
What merchant-of-record actually means
When Paddle is your merchant of record, Paddle is the seller of record to your customers. Your customers are legally buying from Paddle, not from you. Paddle collects the payment, withholds the taxes (VAT in the EU, GST in Australia, sales tax in applicable US states), and remits those taxes to the relevant governments. You receive a payout minus Paddle's fee.
This is genuinely useful. EU VAT compliance is a nightmare for small SaaS shops. MOSS/OSS registration, filing quarterly returns across 27 member states, figuring out the right rate for digital services in Germany vs France vs Hungary - I've helped clients wade through this, and it's not trivial. If your accountant charges by the hour and you're doing this manually, Paddle's overhead starts to look cheap.
But here's the thing people gloss over: Paddle's fee is not just a payment processing fee. It's a tax-and-compliance service bundled into a transaction cut. And that cut comes off your gross revenue, not your net.
The actual fee structure
Paddle Billing currently charges 5% + $0.50 per transaction. Stripe is 2.9% + $0.30 for US cards, with an additional 1.5% for EU cards (so 4.4% + $0.30 on most European transactions).
Let's run a concrete example. Suppose you have a SaaS at $49/month. 60% of your subscribers are in the EU.
Paddle on a $49 EU transaction:
- Fee: 5% + $0.50 = $2.45 + $0.50 = $2.95
- You net: $46.05
Stripe on a $49 EU transaction:
- Fee: 4.4% + $0.30 = $2.16 + $0.30 = $2.46
- You net: $46.54
That's about $0.49 per transaction in Paddle's favor for Stripe. Not a huge deal in isolation. But Paddle's 5% also applies to US transactions where Stripe is only 2.9% + $0.30 = $1.72. On a $49 US transaction, Paddle costs $2.95 and Stripe costs $1.72. That's $1.23 more per US subscriber per month you're paying Paddle.
At 1,000 subscribers with 60% EU, 40% US:
- 400 US subscribers x $1.23 extra = $492/month
- 600 EU subscribers x $0.49 extra = $294/month (Stripe is more expensive here, but you'd need to handle VAT)
- Net extra cost of Paddle vs Stripe: $492 - $294 = $198/month... plus whatever VAT compliance costs you on the Stripe path.
So the real question is: what does EU VAT compliance cost you if you self-manage on Stripe?
The hidden cost of DIY VAT on Stripe
Stripe Tax exists and it's genuinely decent now. It calculates the right tax at checkout, can collect it, and generates the reports you need to file. But it does not file for you. You still need to be registered for EU OSS (One Stop Shop), and you or your accountant still files the returns.
Stripe Tax costs 0.5% per transaction where tax is calculated. So on EU transactions, add 0.5% to Stripe's 4.4% = 4.9% + $0.30. Now the gap with Paddle's 5% + $0.50 is basically nothing on a per-transaction basis, and you're still doing your own filing.
If your accountant charges $200-400/quarter to handle OSS filing (a reasonable number for a small SaaS), that's $800-1600/year in overhead. At the $198/month in savings I calculated above, Stripe + Stripe Tax + accountant still wins financially at scale - but only if you're past maybe 300-400 EU subscribers. Below that threshold, Paddle's all-in pricing is probably worth it just to not think about it.
I helped set this up for a small B2C SaaS client last year. They had maybe 180 EU subscribers at $29/month. Paddle was the right call. The compliance lift alone justified the premium.
Integrating Paddle Billing in Laravel
Paddle has a Laravel package (Laravel Cashier for Paddle) that parallels the Stripe Cashier package, though it's a bit rougher around the edges. Here's what a basic subscription checkout looks like:
// routes/web.php
Route::get('/subscribe', [BillingController::class, 'subscribe'])->middleware('auth');
// BillingController.php
use Illuminate\Http\Request;
use Paddle\Laravel\Facades\Paddle;
class BillingController extends Controller
{
public function subscribe(Request $request)
{
$user = $request->user();
// Create a Paddle customer if one doesn't exist yet
if (! $user->paddle_id) {
$customer = Paddle::customers()->create([
'email' => $user->email,
'name' => $user->name,
]);
$user->update(['paddle_id' => $customer->id]);
}
// Generate a transaction for the checkout overlay
$transaction = Paddle::transactions()->create([
'customer_id' => $user->paddle_id,
'items' => [
[
'price_id' => config('billing.paddle_price_id'),
'quantity' => 1,
],
],
]);
return view('billing.subscribe', [
'transactionId' => $transaction->id,
]);
}
}
{{-- billing/subscribe.blade.php --}}
<x-layout>
<div id="checkout-container"></div>
<script src="https://cdn.paddle.com/paddle/v2/paddle.js"></script>
<script>
Paddle.Setup({
token: '{{ config("billing.paddle_client_token") }}',
});
Paddle.Checkout.open({
transactionId: '{{ $transactionId }}',
settings: {
frameTarget: 'checkout-container',
frameInitialHeight: 450,
frameStyle: 'width: 100%; background-color: transparent; border: none;',
},
});
</script>
</x-layout>
Paddle fires webhooks for subscription events. You listen for subscription.activated, subscription.updated, subscription.canceled, and transaction.completed. Verifying webhook signatures in Laravel looks like this:
// app/Http/Controllers/PaddleWebhookController.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class PaddleWebhookController extends Controller
{
public function handle(Request $request)
{
$signature = $request->header('Paddle-Signature');
$secret = config('billing.paddle_webhook_secret');
$rawBody = $request->getContent();
// Paddle uses a ts=;h1= format
$parts = [];
foreach (explode(';', $signature) as $part) {
[$key, $value] = explode('=', $part, 2);
$parts[$key] = $value;
}
$signed = $parts['ts'] . ':' . $rawBody;
$expected = hash_hmac('sha256', $signed, $secret);
if (! hash_equals($expected, $parts['h1'])) {
return response('Invalid signature', 403);
}
$payload = $request->json()->all();
$eventType = $payload['event_type'] ?? null;
match ($eventType) {
'subscription.activated' => $this->handleActivated($payload['data']),
'subscription.canceled' => $this->handleCanceled($payload['data']),
default => Log::info('Unhandled Paddle event', ['type' => $eventType]),
};
return response('OK', 200);
}
}
Nothing exotic, but notice I'm doing signature verification manually because the Cashier package's built-in webhook handling has had some lag keeping up with Paddle Billing's v2 API changes. Worth double-checking that whatever version you install actually matches the webhook format Paddle is sending.
The gotchas that actually bit me
Customer portal customization is limited. Paddle hosts a customer portal for subscription management, and while it works, you can't style it to match your app. For some SaaS products that's fine. For anything where the billing experience is part of the product feel, it's jarring. Stripe's Billing Portal is also hosted, but it's more configurable.
Refunds are weird. Because Paddle is the merchant of record, refunds run through Paddle. The money comes from Paddle's reserve, not your payout. This sounds fine until you have a dispute-heavy period and start noticing how Paddle manages that reserve. Keep an eye on your payout schedule if refund volume spikes.
Sandbox is separate from production. This is table stakes for payment processors, but Paddle's sandbox environment has historically had differences from production behavior around tax calculation. Test with realistic EU addresses, not just US ones.
Price IDs are immutable in meaningful ways. Once a price is live with subscribers on it, you can't edit the amount. You create a new price and migrate subscribers. Same as Stripe, but I've seen developers not realize this until they tried to do a price change in production.
When I'd reach for Paddle
I'd use Paddle Billing when:
- The product is early stage, EU-heavy, and the founders are not ready to think about tax compliance
- It's a pure subscription play with simple pricing - Paddle's checkout is fast to ship
- The team is small and absorbing $198+/month in Paddle's premium is worth not hiring an accountant or touching Stripe Tax config
I'd stick with Stripe when:
- Revenue is past the point where Paddle's premium materially compresses margin
- The product needs sophisticated billing logic - metered billing, complex entitlements, invoice customization
- The client already has Stripe infrastructure (I'm not migrating payment methods without a compelling reason)
- B2B with invoicing requirements - Stripe's invoicing is more mature for net-30 terms and purchase order workflows
Closing thought
Paddle's merchant-of-record model is a real solution to a real problem, and I don't want to dismiss it - EU VAT compliance is genuinely painful and Paddle takes that pain away cleanly. But it's a service you're buying on every transaction forever, so do the math on your actual subscriber mix before you assume it's cheaper than handling compliance yourself. The crossover point is lower than Paddle's marketing implies.
Need help shipping something like this? Get in touch.