laravel-cashier-bachs maintained by veekthoven
Laravel Cashier (Bachs)
Laravel Cashier Bachs provides an expressive, fluent interface to Bachs' subscription billing services — modeled after Laravel Cashier for Stripe. It handles Bachs customers, hosted checkouts, one-time payments, subscriptions with trials, plan swaps with proration, cancellation grace periods, and webhook-driven state syncing.
[!NOTE] In Bachs, subscriptions are always started by the customer completing a hosted checkout for a recurring product. This package creates the checkout session and then keeps your local subscription records in sync via webhooks.
Installation
Install the package via composer:
composer require veekthoven/laravel-cashier-bachs
Run Cashier's migrations, which add a bachs_id and trial_ends_at column to your users table and create the subscriptions table:
php artisan migrate
You may publish the migrations to customize them:
php artisan vendor:publish --tag="cashier-migrations"
And optionally the config file:
php artisan vendor:publish --tag="cashier-config"
Configuration
Add your Bachs credentials to your .env file:
BACHS_API_KEY=sk_sandbox_...
BACHS_WEBHOOK_SECRET=whsec_...
Keys prefixed with sk_sandbox_ are automatically routed to the Bachs sandbox API and sk_live_ keys to production — no extra configuration needed.
Billable model
Add the Billable trait to your model:
use Veekthoven\CashierBachs\Billable;
class User extends Authenticatable
{
use Billable;
}
If your billable model is not App\Models\User, register it in a service provider:
use Veekthoven\CashierBachs\Cashier;
Cashier::useCustomerModel(Team::class);
Customers
// Create the customer in Bachs (uses the model's email, name and phone)...
$user->createAsBachsCustomer();
// Or with overrides...
$user->createAsBachsCustomer(['name' => 'Jane Doe']);
$user->hasBachsId();
$user->bachsId();
// Retrieve the raw Bachs customer payload...
$customer = $user->asBachsCustomer();
// Push local changes to Bachs...
$user->syncBachsCustomerDetails();
The trait reads email, name and phone from your model by default. Override bachsEmail(), bachsName() or bachsPhone() to customize.
Subscriptions
Creating subscriptions
Subscriptions start with a hosted checkout for a recurring Bachs product:
use Illuminate\Http\Request;
Route::get('/subscribe', function (Request $request) {
return $request->user()
->newSubscription('default', 'prod_abc123')
->checkout([
'success_url' => route('billing.success'),
'cancel_url' => route('billing.cancel'),
]);
});
The Checkout object is responsable — returning it redirects the customer to the Bachs-hosted checkout page. You can also grab the URL yourself with $checkout->url().
Once the customer pays, Bachs sends a customer.subscription.created webhook and Cashier creates the local subscription record automatically. The subscription "type" (default above) travels through the checkout session's metadata.
Checking subscription status
$user->subscribed(); // has a valid "default" subscription
$user->subscribed('default', 'prod_abc123'); // ...to a specific product
$user->subscribedToProduct('prod_abc123');
$subscription = $user->subscription();
$subscription->valid(); // active, trialing, past_due, or on grace period
$subscription->active();
$subscription->onTrial();
$subscription->pastDue();
$subscription->paused();
$subscription->canceled();
$subscription->onGracePeriod(); // canceled but still within the paid period
$subscription->ended();
Trials
Recurring products configured with a trial in Bachs start subscriptions in the trialing state — no extra code needed. You can also manage trials directly:
$subscription->extendTrial(now()->addDays(30));
$subscription->endTrial(); // ends the trial and bills immediately
For trials without requiring a checkout ("generic" trials), set trial_ends_at on the billable model when creating it:
$user = User::create([
// ...
'trial_ends_at' => now()->addDays(14),
]);
$user->onTrial(); // true (generic or subscription trial)
$user->onGenericTrial(); // true (generic trial only)
Swapping plans
$subscription->swap('prod_premium'); // prorated, invoiced now (Bachs default)
$subscription->swapWithoutProration('prod_premium'); // no proration
$subscription->swap('prod_premium', [
'proration_behavior' => 'next_cycle', // settle the difference next cycle
]);
Cancelling
$subscription->cancel(); // at period end; enters a grace period
$subscription->cancelNow(); // immediately
$subscription->cancel('Too expensive'); // with a reason
While on the grace period, $subscription->valid() remains true until the paid period runs out.
Payment method
$subscription->updatePaymentMethod('pm_abc123');
One-time payments
Sell one-time products through a hosted checkout:
// Single product...
return $user->checkout('prod_ebook', [
'success_url' => route('shop.success'),
]);
// Multiple products with quantities...
return $user->checkout([
['product_id' => 'prod_course', 'quantity' => 2],
'prod_ebook',
]);
Refunds
$user->refund('chr_1a2b3c4d5e6f');
$user->refund('chr_1a2b3c4d5e6f', ['amount' => '10.00', 'reason' => 'Requested by customer']);
Webhooks
Cashier registers a webhook route at /bachs/webhook that keeps subscriptions in sync (customer.subscription.created, .updated, .deleted). Create the Bachs webhook endpoint with:
php artisan cashier:webhook
The command prints the signing secret — add it to your .env as BACHS_WEBHOOK_SECRET. Incoming webhooks are verified against the X-Bachs-Signature header (HMAC-SHA256) and stale deliveries are rejected.
[!IMPORTANT] Make sure the webhook route is excluded from CSRF verification. In
bootstrap/app.php:->withMiddleware(function (Middleware $middleware) { $middleware->validateCsrfTokens(except: ['bachs/*']); })
Handling other events
Listen for any Bachs event via the WebhookReceived event:
use Veekthoven\CashierBachs\Events\WebhookReceived;
Event::listen(function (WebhookReceived $event) {
if ($event->payload['type'] === 'invoice.payment_failed') {
// ...
}
});
A WebhookHandled event fires after Cashier has processed one of the events it manages itself.
Low-level API access
Anything not covered by the fluent interface is available through the API client:
use Veekthoven\CashierBachs\Cashier;
$products = Cashier::api()->listProducts();
$payment = Cashier::api()->getPayment('pay_1a2b3c4d5e');
$response = Cashier::api()->get('/accounts/balances');
Failed requests throw Veekthoven\CashierBachs\Exceptions\BachsApiError, exposing the stable errorCode, validation errors, and the requestId for support.
Customization
use Veekthoven\CashierBachs\Cashier;
Cashier::ignoreRoutes(); // don't register the webhook route
Cashier::ignoreMigrations(); // don't load the package migrations
Cashier::useSubscriptionModel(CustomSubscription::class);
Cashier::formatCurrencyUsing(fn ($amount, $currency) => /* ... */);
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Credits
License
The MIT License (MIT). Please see License File for more information.