Paddle Integration for SaaS Subscriptions
Paddle is a payment platform acting as Merchant of Record (MoR): handles taxes (VAT, GST, sales tax), refunds and compliance. For SaaS companies outside US and EU this is often the only way to legally sell in 200+ countries without opening legal entity in each. Paddle calculates and pays local taxes — developers don't need to think about it.
Paddle Billing (v2) is newer product, differs from Paddle Classic API. Integration takes 3–5 working days.
Paddle Billing vs Classic
Paddle Classic uses overlay checkout — JavaScript widget embedded on page. Paddle Billing uses own checkout URL or inline checkout via iframe. New projects should start with Billing if Classic not needed for compatibility.
Creating Products and Prices
// Paddle Billing REST API
const product = await paddle.products.create({
name: 'Pro Plan',
tax_category: 'saas',
});
const price = await paddle.prices.create({
product_id: product.id,
description: 'Pro Monthly',
billing_cycle: { interval: 'month', frequency: 1 },
trial_period: { interval: 'day', frequency: 14 },
unit_price: { amount: '2900', currency_code: 'USD' },
});
Frontend Checkout
Paddle Billing provides @paddle/paddle-js SDK:
import { initializePaddle } from '@paddle/paddle-js';
const paddle = await initializePaddle({
environment: 'production', // or 'sandbox'
token: 'live_...',
eventCallback(event) {
if (event.name === 'checkout.completed') {
const { transaction_id, customer } = event.data;
// Redirect or update UI
window.location.href = `/dashboard?checkout=success&txn=${transaction_id}`;
}
},
});
// Open checkout
paddle.Checkout.open({
items: [{ priceId: 'pri_...', quantity: 1 }],
customer: { email: currentUser.email },
customData: { user_id: currentUser.id },
successUrl: `${window.location.origin}/dashboard`,
});
Webhooks and Synchronization
Paddle sends subscription events. Signatures verified via ECDSA with public key from dashboard:
use Paddle\Webhooks\Verify;
public function handleWebhook(Request $request): Response
{
$verified = Verify::signature(
$request->getContent(),
$request->header('Paddle-Signature'),
config('services.paddle.webhook_secret')
);
if (!$verified) {
return response('Forbidden', 403);
}
$payload = $request->json()->all();
match ($payload['event_type']) {
'subscription.created' => $this->onSubscriptionCreated($payload['data']),
'subscription.updated' => $this->onSubscriptionUpdated($payload['data']),
'subscription.cancelled' => $this->onSubscriptionCancelled($payload['data']),
'transaction.completed' => $this->onTransactionCompleted($payload['data']),
'transaction.payment_failed' => $this->onPaymentFailed($payload['data']),
default => null,
};
return response('OK', 200);
}
private function onSubscriptionCreated(array $data): void
{
$userId = $data['custom_data']['user_id'];
$user = User::findOrFail($userId);
$user->update([
'paddle_subscription_id' => $data['id'],
'paddle_customer_id' => $data['customer_id'],
'subscription_status' => $data['status'],
'plan' => $data['items'][0]['price']['id'],
'trial_ends_at' => $data['current_billing_period']['starts_at'] ?? null,
'renews_at' => $data['next_billed_at'],
]);
}
Upgrade and Cancellation
Paddle Billing allows updating subscription via API. Prorated billing calculated automatically:
// Upgrade to new plan
$paddleClient->subscriptions()->update($subscription_id, [
'items' => [[
'price_id' => 'pri_new_plan',
'quantity' => 1,
]],
'proration_billing_mode' => 'prorated_immediately',
]);
// Cancel at end of period
$paddleClient->subscriptions()->cancel($subscription_id, [
'effective_from' => 'next_billing_period',
]);
Customer Portal
Unlike Stripe, Paddle doesn't have ready Customer Portal. Subscription management implemented via Paddle Update Payment Method URL and own API calls. To change payment method:
$updateUrl = $paddleClient->subscriptions()->getPaymentMethodUpdateTransaction($subscription_id);
// Redirect to $updateUrl->url
Taxes and Currencies
Paddle automatically determines customer country by IP and adds local tax (VAT in EU, GST in Australia etc.). Final price user sees already includes tax. Paddle pays it to local tax authorities — no additional action from developer needed.
Paddle supports dynamic pricing: same product displayed in different currencies depending on user's country.







