As your SaaS business grows, implementing a reliable billing system becomes increasingly critical. Yet, choosing and implementing the right billing infrastructure is often more complex than it appears on the surface. Between handling subscriptions, managing failed payments, staying compliant with tax regulations, and providing a smooth customer experience, there's a lot to consider.
This is where the combination of Laravel and Paddle shines. Laravel's official billing package, Cashier-Paddle, provides an elegant interface to Paddle's subscription billing services, making it significantly easier to implement robust billing functionality in your application.
In this guide, you'll learn:
- Why Paddle might be the right choice for your Laravel application
- How to implement both one-time charges and subscription billing
- Best practices for handling webhooks and managing customers
- Advanced subscription management features for complex business models
- Real-world implementation strategies and common pitfalls to avoid
Let's start by examining why Paddle could be the right choice for your business's billing infrastructure.
Why Choose Paddle for Your Laravel Application?
When evaluating billing solutions for your SaaS application, several providers might come to mind: Stripe, Paddle, Chargebee, and others. However, Paddle offers some unique advantages that make it particularly appealing for growing businesses, especially when combined with Laravel.
Built for Modern SaaS
Unlike traditional payment processors that primarily handle transactions, Paddle operates as a Merchant of Record (MoR). This means they handle not just payments, but also:
- Tax Management: Paddle automatically handles VAT/GST collection and remittance worldwide. As a technical leader, you understand the complexity this eliminates from your system - no need to integrate separate tax calculation services or maintain tax rate databases.
- Global Payments: Support for multiple payment methods and currencies comes out of the box. Your development team won't need to handle currency conversions or maintain multiple payment gateway integrations.
- Subscription Management: The platform provides robust subscription management capabilities, including trial periods, cancellations, and upgrades/downgrades.
Laravel Integration Through Cashier
Laravel's official support for Paddle through Cashier is a significant advantage. Here's a practical example of how clean the implementation can be:
// Creating a subscription checkout session
Route::get('/subscribe', function (Request $request) {
$checkout = $request->user()->subscribe('price_monthly')
->returnTo(route('dashboard'));
return view('billing', ['checkout' => $checkout]);
});
This simple code handles creating a subscription, managing the customer record, and setting up webhook handling - tasks that would typically require significantly more code with other providers.
Real-world Benefits
From a technical leadership perspective, here are the tangible benefits:
- Reduced Development Time: The Laravel Cashier package eliminates boilerplate code for common billing operations. Your team can focus on building core business features rather than billing infrastructure.
- Lower Maintenance Burden: With Paddle handling tax compliance and payment methods, your team won't need to maintain these complex systems.
- Simplified Architecture: As Paddle acts as the Merchant of Record, many billing-related microservices and databases become unnecessary, leading to a simpler system architecture.
- Built-in Compliance: Paddle's MoR status means they handle most financial compliance requirements, reducing your compliance burden and associated costs.
When Paddle Might Not Be the Best Choice
In the interest of making an informed decision, there are scenarios where Paddle might not be ideal:
- If you need granular control over the payment flow
- If your business model requires direct access to payment methods
- If Paddle's higher fees compared to direct payment processors don't align with your margins
Let's move on to implementing Paddle in your Laravel application.
Getting Started with Laravel Cashier (Paddle)
For technical teams migrating to or implementing Paddle, the setup process is straightforward but requires attention to detail. Let's walk through the implementation step by step.
Paddle Sandbox account
In order to first test your implementation in a test environment, Paddle offers "Sandbox" accounts which are separate from your main production accounts.
You can sign up for a Paddle Sandbox account here - https://sandbox-login.paddle.com/signup
Initial Setup
First, install the Cashier package via Composer:
composer require laravel/cashier-paddle
Then, you'll need to prepare your database. Cashier provides migrations for all necessary tables:
php artisan vendor:publish --tag="cashier-migrations"
php artisan migrate
Configuration Essentials
The setup requires configuration both in your Laravel application and in your Paddle dashboard. Here's a production-ready configuration approach:
// config/services.php or .env file
PADDLE_SANDBOX=false
PADDLE_CLIENT_SIDE_TOKEN=your-paddle-client-side-token
PADDLE_API_KEY=your-paddle-api-key
PADDLE_WEBHOOK_SECRET=your-paddle-webhook-secret
For testing and developing locally, you might want to switch PADDLE_SANDBOX=false
and update credentials from your Paddle Sandbox account.
Making Your Model Billable
Add the Billable
trait to your User model (or any model that needs billing capabilities):
use Laravel\Paddle\Billable;
class User extends Authenticatable
{
use Billable;
}
Implementing the Checkout Flow
Here's a practical implementation of a subscription checkout flow:
use Illuminate\Http\Request;
class SubscriptionController extends Controller
{
public function checkout(Request $request)
{
// Create a checkout session with a 14-day trial
$checkout = $request->user()
->checkout('pri_monthly_plan')
->returnTo(route('dashboard'))
->customData(['business_size' => $request->business_size]);
return view('billing.checkout', compact('checkout'));
}
}
In your view:
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
Start Premium Plan
</x-paddle-button>
Critical: Webhook Handling
One of the most important aspects of a billing system is properly handling webhooks. Here's a robust webhook setup:
// app/Providers/EventServiceProvider.php
use Laravel\Paddle\Events\SubscriptionCreated;
use Laravel\Paddle\Events\SubscriptionCanceled;
protected $listen = [
SubscriptionCreated::class => [
HandleSubscriptionCreated::class,
],
SubscriptionCanceled::class => [
HandleSubscriptionCanceled::class,
],
];
Example webhook handler:
class HandleSubscriptionCreated
{
public function handle(SubscriptionCreated $event)
{
$user = $event->billable;
$subscription = $event->subscription;
// Provision resources for the new subscriber
ProvisionUserResources::dispatch($user);
// Notify relevant teams
NotifyCustomerSuccess::dispatch($user);
}
}
Testing Your Implementation
Before going live, test your implementation thoroughly in Paddle's sandbox environment:
// .env.testing
PADDLE_SANDBOX=true
And set up test cases:
public function test_user_can_subscribe()
{
$user = User::factory()->create();
$response = $this->actingAs($user)
->post('/subscription/create', [
'plan' => 'pri_monthly_plan'
]);
$this->assertTrue($user->fresh()->subscribed());
}
Important Production Considerations
Before deploying to production:
- Webhook Security: Ensure your webhook endpoint is properly secured:
// config/cashier.php
'webhook_secret' => env('PADDLE_WEBHOOK_SECRET'),
To get the secret and learn more about them, please visit Paddle's webhook signatures help article.
- Error Handling: Implement robust error handling for failed payments:
use Laravel\Paddle\Events\WebhookReceived;
public function handle(WebhookReceived $event)
{
if ($event->payload['alert_name'] === 'subscription_payment_failed') {
// Handle failed payment
NotifyUserOfFailedPayment::dispatch($event->billable);
}
}
- Monitoring: Set up monitoring for webhook processing:
Log::channel('billing')->info('Webhook processed', [
'event' => $event->payload['alert_name'],
'customer' => $event->billable->id
]);
Advanced Features and Business Benefits
Let's explore some of Paddle's advanced features that can provide significant business value. We'll focus on implementations that solve real business challenges. Here are just a few examples that you could implement to improve revenue collection.
Flexible Subscription Management
One of the most common requirements for SaaS businesses is handling plan changes. Here's how to implement a robust plan switching system:
class SubscriptionController extends Controller
{
public function switchPlan(Request $request)
{
$subscription = $request->user()->subscription();
// Handle proration based on business rules
if ($request->prorate) {
$subscription->swap($request->new_price_id);
} else {
$subscription->noProrate()->swap($request->new_price_id);
}
// Optionally, immediately invoice the customer
if ($request->bill_now) {
$subscription->swapAndInvoice($request->new_price_id);
}
return response()->json(['message' => 'Plan updated successfully']);
}
}
Implementing Trials That Convert
Trials are crucial for SaaS growth. Here's a pattern that maximizes trial effectiveness:
class TrialController extends Controller
{
public function startTrial(Request $request)
{
// Create customer with trial
$user = $request->user();
if (!$user->customer) {
$user->createAsCustomer([
'trial_ends_at' => now()->addDays(14)
]);
}
// Provision trial features
ProvisionTrialFeatures::dispatch($user);
// Schedule trial ending notification
SendTrialEndingNotification::dispatch($user)
->delay(now()->addDays(12));
return redirect()->route('dashboard');
}
}
Handling Payment Failures Gracefully
Payment failures are inevitable. Here's a robust recovery system:
class PaymentFailureHandler
{
public function handle(WebhookReceived $event)
{
if ($event->payload['event_type'] === 'subscription_payment_failed') {
$user = $event->billable;
// Give users a grace period
if (!$user->subscription()->pastDue()) {
NotifyPaymentFailed::dispatch($user);
return;
}
// After grace period, restrict access
if ($user->subscription()->pastDue()) {
RestrictAccountAccess::dispatch($user);
SendFinalPaymentNotice::dispatch($user);
}
}
}
}
Business Intelligence Through Transactions
Paddle provides rich transaction data. Here's how to leverage it for business insights:
use Laravel\Paddle\Subscription;
use Laravel\Paddle\Transaction;
class RevenueAnalytics
{
public function getMRR()
{
return Transaction::query()
->whereMonth('billed_at', now()->month)
->whereYear('billed_at', now()->year)
->whereNotNull('subscription_id')
->sum('total');
}
public function getChurnRate()
{
$startOfMonth = now()->startOfMonth();
$totalSubscribers = Subscription::where('created_at', '<', $startOfMonth)->count();
$churnedSubscribers = Subscription::where('canceled_at', '>=', $startOfMonth)->count();
return $churnedSubscribers / $totalSubscribers * 100;
}
}
Multi-Product Subscriptions
For businesses offering multiple products or add-ons:
class ProductController extends Controller
{
public function addProduct(Request $request)
{
$subscription = $request->user()->subscription();
// Add new product to existing subscription
$subscription->swap([
'price_existing_product',
'price_new_product' => $request->quantity
]);
return back()->with('status', 'Product added to subscription');
}
}
Real-world Implementation Tips and Conclusion
Common Pitfalls and Their Solutions
1. Webhook Reliability
One of the most critical aspects of billing infrastructure is webhook handling. A mishandled webhook can mean data going out of sync, users not receiving their benefits, or notifications of failed payments. Here's a more robust implementation, which can be improved even more if needed:
class WebhookController extends Controller
{
public function handleWebhook(Request $request)
{
try {
// Log incoming webhook for debugging
Log::info('Webhook received', [
'event' => $request->input('event_type'),
'payload' => $request->all()
]);
// Process webhook with retries
DB::transaction(function () use ($request) {
// Cashier's built-in webhook handling
return $this->handlePaddleWebhook($request);
}, 5);
} catch (Exception $e) {
Log::error('Webhook processing failed', [
'error' => $e->getMessage(),
'event' => $request->input('event_type')
]);
// Don't expose internal errors to Paddle
return response()->json(['message' => 'Webhook processed']);
}
}
}
2. Testing Environment Setup
Set up a proper testing environment to avoid surprises. Testing various billing endpoints is a great way to ensure confidence and continuity of your billing operations. You wouldn't want a random bug to cause you to lose money.
To write robust tests, you can mock API calls to Paddle and simulate various scenarios. You can learn more about the different Paddle API endpoints and their responses in Paddle API Documentation.
class TestCase extends BaseTestCase
{
protected function setUp(): void
{
parent::setUp();
config(['cashier.sandbox' => true]);
// Mock Paddle API responses for tests
Http::fake([
'sandbox-checkout.paddle.com/*' => Http::response([
'success' => true,
'response' => [
'subscription_id' => 'sub_123',
'customer_id' => 'cus_123'
]
], 200)
]);
}
}
Production Readiness Checklist
Before going live:
- Environment Configuration: make sure you have updated your
.env
variables in production with the correct API keys and secrets. Do NOT use your Sandbox environment variables in production!
// .env
PADDLE_SANDBOX=false
PADDLE_CLIENT_SIDE_TOKEN=your-paddle-client-side-token
PADDLE_API_KEY=your-paddle-api-key
PADDLE_WEBHOOK_SECRET=your-paddle-webhook-secret
- Error Monitoring: make sure you are notified immediately via Slack or email about failed payments or other billing failures.
class BillingServiceProvider extends ServiceProvider
{
public function boot()
{
// Monitor failed webhooks
Event::listen(WebhookHandled::class, function ($event) {
if ($event->payload['event_type'] === 'subscription_payment_failed') {
Notification::route('slack', env('BILLING_SLACK_WEBHOOK'))
->notify(new FailedPaymentNotification($event->payload));
}
});
}
}
- Customer Support Tools: build a way for your customers to view their billing information, invoices, and manage their subscription.
class BillingPortalController extends Controller
{
public function show(User $user)
{
return view('admin.billing', [
'subscriptions' => $user->subscriptions()->with('transactions')->get(),
'paymentHistory' => $user->transactions()->latest()->get(),
'customerInfo' => $user->paddleCustomer()
]);
}
}
Business Impact Monitoring
Track key metrics to measure the success of your billing implementation. You can't improve what you don't track, so make sure to develop and track key metrics for your business, so you can optimise the billing solution even further.
class BillingMetrics
{
public function getKeyMetrics()
{
return [
'mrr' => $this->calculateMRR(),
'churn_rate' => $this->calculateChurnRate(),
'average_revenue_per_user' => $this->calculateARPU(),
'failed_payment_recovery' => $this->calculateRecoveryRate()
];
}
private function calculateRecoveryRate()
{
$failedPayments = Transaction::where('status', 'failed')
->whereMonth('created_at', now()->month)
->count();
$recoveredPayments = Transaction::where('status', 'completed')
->whereHas('previousAttempt')
->whereMonth('created_at', now()->month)
->count();
return $failedPayments > 0
? ($recoveredPayments / $failedPayments) * 100
: 100;
}
}
Conclusion
Implementing Paddle with Laravel through Cashier provides a robust billing infrastructure that can scale with your business. Key takeaways:
- Use webhooks properly - they're the backbone of your billing system
- Implement proper error handling and monitoring
- Take advantage of Paddle's MoR status for tax and compliance
- Use Cashier's built-in features to reduce custom code
- Monitor key metrics to ensure business success
Remember that a billing system is critical infrastructure - invest time in proper implementation and testing. The combination of Laravel and Paddle provides a solid foundation that can support complex billing needs while keeping the codebase clean and maintainable.
For next steps:
- Set up a sandbox environment for testing
- Implement core subscription flows
- Set up proper monitoring and alerts
- Test extensively with different scenarios
- Plan for gradual rollout to production
Stay updated with both Laravel Cashier and Paddle documentation for new features and best practices as they evolve.
Want to skip the complexity of building and maintaining your own billing infrastructure?
Spike Billing provides a complete, self-hosted billing solution that integrates seamlessly with both Stripe and Paddle. Your customers get a fully-featured portal to manage their subscriptions, one-time purchases, and billing details, while you maintain full control over the experience.
Get started in minutes with our Laravel package, or explore our extensive customization options to match your specific business needs. Visit documentation to get started, or have a look at a live demo.