# How to Add Payments to a Laravel SaaS

> Complete guide to integrating payment processing in Laravel applications. Covers checkout integration, webhook handling, subscription management, and global tax compliance with Dodo Payments.
- **Author**: Ayush Agarwal
- **Published**: 2026-03-24
- **Category**: Payments, Developer Tools, How-To
- **URL**: https://dodopayments.com/blogs/add-payments-laravel-saas

---

Laravel is the backbone of thousands of SaaS products, from small indie hacker projects to massive enterprise platforms. Its elegant syntax and robust ecosystem make it the go-to choice for developers who want to build and scale quickly. However, when it comes to the "money" part of the equation, many developers find themselves stuck in a maze of complex billing logic, tax compliance, and subscription management.

The default path for most Laravel developers is to reach for Stripe and Laravel Cashier. While Cashier is a fantastic tool, it still leaves you with the heavy lifting of handling global sales tax, VAT, and the legal complexities of being the seller of record. If you are selling to customers in 50 different countries, you are suddenly responsible for registering, collecting, and remitting taxes in 50 different jurisdictions.

This is where Dodo Payments changes the game. By acting as a [Merchant of Record for SaaS](https://dodopayments.com/blogs/merchant-of-record-for-saas), Dodo Payments handles the entire tax and compliance burden for you. You don't just get a payment gateway; you get a global tax department. In this guide, we will walk through how to integrate Dodo Payments into a Laravel application to create a seamless, tax-compliant checkout experience.

## Why Laravel and Dodo Payments?

Laravel's philosophy is about developer happiness and productivity. Dodo Payments shares this philosophy by simplifying the most painful part of running a SaaS: global payments. When you combine the two, you get a powerhouse stack that allows you to focus on your core product while your billing infrastructure "just works." This synergy is particularly evident when you consider the rapid development cycles of modern SaaS startups. You can go from a fresh Laravel installation to a revenue-generating product in a single afternoon.
One of the biggest advantages is the [embedded payments for SaaS](https://dodopayments.com/blogs/embedded-payments-saas) approach. Instead of redirecting users to a clunky external hosted page, you can use Dodo's overlay or inline checkout. This keeps users on your site, increasing conversion rates and providing a more professional feel. In the world of SaaS, every friction point in the checkout process is a potential lost customer. By keeping the experience native to your application, you maintain trust and brand consistency.
Furthermore, Dodo's [payments architecture for SaaS](https://dodopayments.com/blogs/payments-architecture-saas) is designed for modern subscription models. Whether you are doing flat-rate billing, tiered pricing, or usage-based models, the integration remains clean and manageable within your Laravel codebase. This flexibility is crucial as your business grows and your pricing strategy evolves. You won't find yourself trapped in a rigid billing system that requires a complete rewrite every time you want to experiment with a new pricing tier.

> Most SaaS founders underestimate the cost of tax compliance. It is not just filing returns. It is registration, calculation at checkout, remittance, and audit readiness across every jurisdiction where you have customers.
>
> \- Ayush Agarwal, Co-founder & CPTO at Dodo Payments

## The Laravel Payment Flow

Before we dive into the code, let's look at how the data flows between your Laravel application and Dodo Payments. This high-level overview will help you understand where each piece of the integration fits.

```mermaid
flowchart TD
    A[User clicks 'Subscribe'] --> B[Laravel Controller]
    B -->|"Create Checkout Session"| C[Dodo Payments API]
    C -->|"Return Checkout URL"| B
    B --> D[Blade View / Livewire]
    D -->|"Open Overlay"| E[Dodo Checkout]
    E -->|"Payment Success"| F[Dodo Webhook]
    F --> G[Laravel Webhook Route]
    G -->|"Process Event"| H[Laravel Queue]
    H --> I[Update Database]
    I --> J[Middleware Grants Access]
```

This flow ensures that your application remains the source of truth for user access while Dodo handles the secure transaction and tax calculations.

## Step 1: Setting Up Your Dodo Account

First, you need to create an account at Dodo Payments. Once you are in the dashboard, you can create your products and prices. For a SaaS, you will typically create a "Subscription" product with monthly or yearly pricing.

Make sure to grab your API key from the developer settings. You should add this to your Laravel `.env` file immediately:

```env
DODO_PAYMENTS_API_KEY=your_api_key_here
DODO_PAYMENTS_WEBHOOK_SECRET=your_webhook_secret_here
DODO_PAYMENTS_MODE=test
```

Using environment variables is a standard Laravel practice that keeps your credentials secure and allows you to switch between test and live modes easily.

## Step 2: Installing the HTTP Client

While Dodo has [SDKs](https://docs.dodopayments.com/developer-resources/dodo-payments-sdks) for various languages, Laravel's built-in `Http` facade is incredibly powerful for making API requests. We will use it to interact with the Dodo API.

If you prefer a more structured approach, you can also use the Dodo Payments Node SDK if you are using a hybrid stack, but for a pure Laravel app, the `Http` facade is often the cleanest path.

## Step 3: Creating a Checkout Session

When a user wants to subscribe, you need to create a checkout session. This session tells Dodo what the user is buying and where to send them after the payment is complete.

Create a controller called `SubscriptionController`:

```php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

class SubscriptionController extends Controller
{
    public function createSession(Request $request)
    {
        $user = $request->user();

        $response = Http::withToken(config('services.dodo.key'))
            ->post('https://test.dodopayments.com/checkouts', [
                'product_cart' => [
                    [
                        'product_id' => 'pdt_your_product_id',
                        'quantity' => 1,
                    ]
                ],
                'customer' => [
                    'email' => $user->email,
                    'name' => $user->name,
                ],
                'billing' => [
                    'country' => 'US', // You can collect this or default it
                ],
                'payment_link' => true,
                'return_url' => route('subscription.success'),
            ]);
        if ($response->failed()) {
            return back()->withErrors('Could not create checkout session.');
        }

        return response()->json([
            'checkout_url' => $response->json('checkout_url')
        ]);
    }
}
```

This controller takes the authenticated user's details and sends them to Dodo. Dodo returns a `checkout_url` which we will use in our frontend.

## Step 4: Integrating the Overlay Checkout

Now that we have a checkout URL, we need to show the checkout to the user. Dodo's [overlay checkout](https://docs.dodopayments.com/developer-resources/overlay-checkout) is perfect for this. It provides a modal experience that doesn't require the user to leave your site.

In your Blade template (e.g., `resources/views/billing.blade.php`), add the Dodo Checkout script:

```html
<script src="https://cdn.jsdelivr.net/npm/dodopayments-checkout@latest/dist/index.js"></script>
<script>
  DodoPaymentsCheckout.DodoPayments.Initialize({
    mode: "{{ config('services.dodo.mode') }}",
    displayType: "overlay",
  });

  async function handleSubscribe() {
    const response = await fetch("{{ route('subscription.session') }}", {
      method: "POST",
      headers: {
        "X-CSRF-TOKEN": "{{ csrf_token() }}",
        "Content-Type": "application/json",
      },
    });

    const data = await response.json();

    if (data.checkout_url) {
      DodoPaymentsCheckout.DodoPayments.Checkout.open({
        checkoutUrl: data.checkout_url,
      });
    }
  }
</script>

<button onclick="handleSubscribe()" class="btn btn-primary">
  Upgrade to Pro
</button>
```

This setup provides a smooth transition from your app's UI to the payment form. The user enters their details, completes the payment, and the overlay closes, returning them to your application.

## Step 5: Handling Webhooks

Webhooks are critical for any [best subscription billing software](https://dodopayments.com/blogs/best-subscription-billing-software) integration. They allow Dodo to notify your Laravel app when a payment succeeds, a subscription is renewed, or a payment fails.

First, create a route for the webhook in `routes/api.php`:

```php
Route::post('/webhooks/dodo', [WebhookController::class, 'handle']);
```

Next, create the `WebhookController`. It is a good idea to use Laravel's [integration guide](https://docs.dodopayments.com/developer-resources/integration-guide) principles to ensure your webhook handler is secure.

```php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Support\Facades\Log;

class WebhookController extends Controller
{
    public function handle(Request $request)
    {
        $payload = $request->all();
        $header = $request->header('X-Dodo-Signature');

        // Verify signature here using your secret
        if (!$this->verifySignature($request->getContent(), $header)) {
            return response()->json(['error' => 'Invalid signature'], 400);
        }

        $eventType = $payload['event_type'];

        switch ($eventType) {
            case 'subscription.created':
                $this->handleSubscriptionCreated($payload['data']);
                break;
            case 'payment.succeeded':
                $this->handlePaymentSucceeded($payload['data']);
                break;
            case 'subscription.cancelled':
                $this->handleSubscriptionCancelled($payload['data']);
                break;
        }

        return response()->json(['status' => 'success']);
    }

    protected function handleSubscriptionCreated($data)
    {
        $user = User::where('email', $data['customer']['email'])->first();
        if ($user) {
            $user->update([
                'subscription_id' => $data['subscription_id'],
                'plan' => 'pro',
                'subscription_status' => 'active',
            ]);
        }
    }

    protected function verifySignature($payload, $signature)
    {
        $secret = config('services.dodo.webhook_secret');
        $computed = hash_hmac('sha256', $payload, $secret);
        return hash_equals($computed, $signature);
    }
}
```

Don't forget to exclude this route from CSRF protection in `app/Http/Middleware/VerifyCsrfToken.php` (or the equivalent in newer Laravel versions).

## Step 6: Protecting Routes with Middleware

Once the webhook updates your database, you need to ensure that only active subscribers can access certain parts of your SaaS. Laravel's middleware is the perfect tool for this.

Create a middleware called `Subscribed`:

```php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class Subscribed
{
    public function handle(Request $request, Closure $next)
    {
        if (!$request->user() || $request->user()->subscription_status !== 'active') {
            return redirect()->route('billing')->with('error', 'You need an active subscription.');
        }

        return $next($request);
    }
}
```

Register this middleware in your kernel and apply it to your protected routes:

```php
Route::middleware(['auth', 'subscribed'])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
    Route::get('/settings/pro', [SettingsController::class, 'proFeatures']);
});
```

This simple check ensures that your [subscription pricing models](https://dodopayments.com/blogs/subscription-pricing-models) are enforced across your entire application.

## Advanced: Using Laravel Queues for Webhooks

In a production environment, you should never process heavy logic directly inside a webhook controller. If your database update takes too long or you need to send a "Welcome" email, the webhook request might time out, causing Dodo to retry the event.

Instead, dispatch a job:

```php
public function handle(Request $request)
{
    // ... verification logic ...

    ProcessDodoWebhook::dispatch($request->all());

    return response()->json(['status' => 'accepted'], 202);
}
```

This allows your app to respond to Dodo instantly while the actual work happens in the background. This is a core part of a [how to accept online payments](https://dodopayments.com/blogs/how-to-accept-online-payments) strategy that scales.

## Testing Your Integration

Laravel provides excellent testing tools. You can mock the Dodo API responses to test your controller logic without making real network calls.

```php
public function test_user_can_create_checkout_session()
{
    Http::fake([
        'test.dodopayments.com/*' => Http::response(['checkout_url' => 'https://checkout.dodopayments.com/session/cks_123'], 200),
    ]);

    $user = User::factory()->create();

    $response = $this->actingAs($user)->postJson('/api/subscription/session');

    $response->assertStatus(200)
             ->assertJson(['checkout_url' => 'https://checkout.dodopayments.com/session/cks_123']);

For webhooks, you can use `php artisan make:test WebhookTest` and send POST requests to your webhook endpoint with sample payloads. This ensures your [how to sell software online](https://dodopayments.com/blogs/how-to-sell-software-online) flow is bulletproof before you go live.

## Managing the Subscription Lifecycle

A successful SaaS integration goes beyond just the initial checkout. You need to handle the entire lifecycle of a subscription, including renewals, cancellations, and plan changes. Dodo Payments makes this easy by providing a comprehensive set of webhooks and API endpoints.

When a subscription is renewed, Dodo sends a `subscription.renewed` event. Your Laravel application should listen for this event to extend the user's access. Similarly, if a user cancels their subscription, you will receive a `subscription.cancelled` event. Instead of immediately revoking access, it is often better to set a `trial_ends_at` or `subscription_ends_at` date in your database, allowing the user to continue using the service until the end of their current billing period.

For plan changes, you can use the Dodo API to update the subscription. This is useful for "Pro" to "Enterprise" upgrades. Dodo handles the proration automatically, so you don't have to calculate how much to charge the user for the remaining days of the month. This level of automation is what separates a professional billing setup from a fragile, home-grown solution.

## Providing a Customer Portal

Modern SaaS users expect to be able to manage their own subscriptions. They want to update their credit card details, view their billing history, and download invoices without having to contact support. Dodo Payments provides a built-in customer portal that you can link to directly from your Laravel application.

You can generate a portal link via the API and redirect your users to it. This saves you from having to build complex "Billing Settings" pages in your application. The portal is fully hosted by Dodo and handles all the security and compliance requirements for managing sensitive payment information.

In your Laravel application, you might add a "Manage Billing" button like this:

```php
public function redirectToPortal(Request $request)
{
    $response = Http::withToken(config('services.dodo.key'))
        ->get("https://test.dodopayments.com/customers/{$request->user()->dodo_customer_id}/portal");

    return redirect($response->json('portal_url'));
}
```

This approach keeps your application lean and focused on its core value proposition while providing a top-tier experience for your customers.

## Global Tax Compliance Made Easy

The real magic of using Dodo with Laravel is that you stop worrying about tax. When a customer from Germany buys your SaaS, Dodo calculates the correct VAT, collects it, and handles the filing. Your Laravel app just receives a "payment succeeded" event.

This is why Dodo is often considered the [best platform to sell digital products](https://dodopayments.com/blogs/best-platform-sell-digital-products). You get the power of a custom Laravel application with the compliance peace of mind of a managed marketplace.

## FAQ

### Does Dodo Payments support Laravel Cashier?

Dodo Payments does not currently use Laravel Cashier because Cashier is specifically built for Stripe. However, Dodo provides a much simpler integration path that replaces the need for Cashier's complex local database synchronization. You can manage your subscriptions directly through Dodo's API and webhooks with much less code.

### How do I handle subscription upgrades and downgrades?

You can handle upgrades and downgrades by calling the Dodo Payments API to update the subscription's product ID. When the change is made, Dodo will send a webhook to your Laravel application, allowing you to update the user's plan in your database. Dodo handles the proration calculations automatically.

### Can I use Dodo Payments with Laravel Livewire?

Yes, Dodo Payments works perfectly with Livewire. You can trigger the checkout overlay from a Livewire component by emitting a browser event or using the `wire:click` directive to call a JavaScript function that opens the Dodo checkout. This allows for a highly dynamic and reactive billing interface.

### Is it possible to offer free trials with Dodo and Laravel?

Absolutely. You can configure your products in the Dodo dashboard to include a trial period. When a user signs up, Dodo will collect their payment information but won't charge them until the trial ends. Your Laravel app will receive a webhook when the trial starts and another when the first successful payment is made.

### How does Dodo handle failed payments in Laravel?

When a payment fails, Dodo sends a `payment.failed` webhook. You should set up a listener in your Laravel app to catch this event and update the user's `subscription_status` to `past_due` or `inactive`. You can then use middleware to redirect the user to a "Update Payment Method" page.

## Conclusion

Adding payments to a Laravel SaaS doesn't have to be a month-long project involving tax lawyers and complex billing engines. By leveraging Dodo Payments as your Merchant of Record, you can implement a world-class, globally compliant billing system in a matter of hours.

From the initial [checkout session](https://docs.dodopayments.com/api-reference/introduction) to the final [webhook handling](https://docs.dodopayments.com/developer-resources/webhooks), the integration is clean, secure, and developer-friendly. Whether you are building the next big AI tool or a niche B2B platform, Dodo and Laravel provide the perfect foundation for your growth.

Ready to start selling? Check out the [Dodo Payments pricing](https://dodopayments.com/pricing) and sign up for a developer account today.
---
- [More Payments articles](https://dodopayments.com/blogs/category/payments)
- [All articles](https://dodopayments.com/blogs)