DEV Community

Ivanka Todorova
Ivanka Todorova

Posted on • Updated on

Laravel Cashier: Multiple Stripe accounts based on Billable's property

If your application bills customers internationally, sometimes you need multiple Stripe Accounts for all your companies.

Laravel Cashier doesn't support charging customers from different Stripe accounts out-of-the box, but achieving this is pretty easy.

I will not go through the whole setup of Cashier for a single Stripe account, because Laravel's documentation is fantastic: thorough and easy to follow: Laravel Cashier. The account you setup here, will be the fallback/default account if the Billable model (usually App\Models\User) can't be used to resolve the correct Stripe account.

After you're done setting Cashier up, create a new trait called MyBillable. I tend to keep my traits in a folder inside where they are used. Meaning MyBillable trait will go inside traits folder located in app/models.

After creating the App\Models\Traits\MyBillable, add this as content:

<?php
namespace App\Models\Traits;


use Laravel\Cashier\Billable;
use Laravel\Cashier\Cashier;

trait MyBillable
{
    use Billable;


    /**
     *  Override stripe configuration based on User's country preference
     *
     * @param  array  $options
     * @return \Stripe\StripeClient
     */
    public function stripe(array $options = [])
    {

        return Cashier::stripe($options);
    }
}
Enter fullscreen mode Exit fullscreen mode

The code above does nothing for now, but we will expand the App\Models\Traits\MyBillable::stripe() method later.

If you have followed the setup instructions from the official documentation, your billable model (usually App\Models\User) has the Laravel\Cashier\Cashier\Billable trait used. Go ahead and replace it with our new one MyBillable (which also leverages all provided functionality in Cashier by use-ing it inside).

Verify your application is working properly, as if we haven't made any changes after the Cashier setup.

Inside your App\Models\Traits\MyBillable::stripe() method you can use $this to access your User model and write your own logic to determine which of your Stripe accounts is correct for that customer.

In my case, my users belongsTo countries and each country has country code which I use to get previously set .env variables with my stripe secret, stripe key and currency for Cashier.

To do so, create a new config file billing.php inside config/ folder:

return [
    'stripe' => [
        'bg' => [
            'key' => env('BG_STRIPE_KEY'),
            'secret' => env('BG_STRIPE_SECRET'),
            'currency' => env('BG_CASHIER_CURRENCY'),
        ],
        'pl' => [
            'key' => env('PL_STRIPE_KEY'),
            'secret' => env('PL_STRIPE_SECRET'),
            'currency' => env('PL_CASHIER_CURRENCY'),
        ]
    ],
];
Enter fullscreen mode Exit fullscreen mode

Modify your .env key and add:

# Bulgaria Cashier Setup
BG_STRIPE_KEY=
BG_STRIPE_SECRET=
BG_CASHIER_CURRENCY=
Enter fullscreen mode Exit fullscreen mode
# Poland Cashier Setup
PL_STRIPE_KEY=
PL_STRIPE_SECRET=
PL_CASHIER_CURRENCY=
Enter fullscreen mode Exit fullscreen mode

Lastly, update the method in your MyBillable trait to:

public function stripe(array $options = [])
{
    if (!is_null($this->country)) {
        // Get country specific cashier key
        $config = config('billing.stripe');
        if (array_key_exists($this->country->code, $config)) {
            // Update cashier's config
            config(['cashier.key' => $config[$this->country->code]['key']]);
            config(['cashier.secret' => $config[$this->country->code]['secret']]);
            config(['cashier.currency' => $config[$this->country->code]['currency']]);
        }
    }
    // use default config
    return Cashier::stripe($options);
}
Enter fullscreen mode Exit fullscreen mode

The method above is called every time Cashier needs an instance of StripeClient (provided by Cashier's dependency: stripe-php), so by altering Cashier's config we are getting an instance of the StripeClient for the specified Stripe account.

That's it! You are now creating customers in the appropriate Stripe account based on your logic.

What's next? Cashier also provides webhooks to tell your application about changes made outside of it (related to your customers, subscriptions, payments, etc).

In my next post I will explain how to create different endpoints in your application to handle the calls issued by your multiple Stripe accounts

Check out the comments below to see a way of handling webhooks.

Top comments (4)

Collapse
 
maneeshmk profile image
Maneesh MK

Minor correction. It should be "stripeOptions" instead of "stripe"

public function stripeOptions(array $options = [])
{
if (!is_null($this->country)) {
// Get country specific cashier key
$config = config('billing.stripe');
if (array_key_exists($this->country->code, $config)) {
// Update cashier's config
config(['cashier.key' => $config[$this->country->code]['key']]);
config(['cashier.secret' => $config[$this->country->code]['secret']]);
config(['cashier.currency' => $config[$this->country->code]['currency']]);
}
}
// use default config
return Cashier::stripeOptions($options);
}

Collapse
 
fakeheal profile image
Ivanka Todorova • Edited

Thank you for your comment!

Laravel's Cashier latest version (as of writing of this comment) is 13 and I am setting config() variables before calling the following method: Cashier::stripe().

Collapse
 
jimmyhowe profile image
Jimmy Howe

Any updates on the webhook part? Working through this problem myself

Collapse
 
fakeheal profile image
Ivanka Todorova • Edited

Hey,

here's what I ended up doing, however I am not sure if it's the best approach, but it might help you.

Controller that just extends the one provided by Cashier*:

use Laravel\Cashier\Http\Controllers\WebhookController as CashierController;

class WebhookController extends CashierController
{

}
Enter fullscreen mode Exit fullscreen mode

Register a route in web.php:

Route::post(
    '/stripe/webhook/{code}',
    [WebhookController::class, 'handleWebhook']
)->name('cashier.webhook')
->middleware('set.webhook.secret');
Enter fullscreen mode Exit fullscreen mode

Define a middlewareset.webhook.secret that sets credentials for Cashier based on the parameter $code in the URL:

class SetWebhookSecret
{
    public function handle($request, Closure $next)
    {
        $code = $request->route('code');

        // better use `config()` here
        $webhookSecret = env(
            strtoupper($code).'_STRIPE_WEBHOOK_SECRET',
            env('STRIPE_WEBHOOK_SECRET')
        );

        if ($webhookSecret) {
            Log::channel('stripe')->error('No webhook secret found for: '.$code);
        }

        config(['cashier.webhook.secret' => $webhookSecret]);

        return $next($request);
    }
}
Enter fullscreen mode Exit fullscreen mode

In Stripe define different webhook url's for your different accounts. For example, I have:

api.my-project.com/stripe/webhook/bg
api.my-project.com/stripe/webhook/pl

and in .env:

PL_STRIPE_WEBHOOK_SECRET=secret-pl
BG_STRIPE_WEBHOOK_SECRET=secret-bg
Enter fullscreen mode Exit fullscreen mode

* This is optional, I think, you might want to use the Webhook controller provided by directly when defining the route & applying the middleware to it. I have a bunch of other stuff there that aren't related to handling multiple Stripe accounts.