mollie/laravel-cashier-mollie
Laravel Cashier Mollie provides an expressive, fluent interface to Mollie payments for Laravel. Build subscription billing, recurring charges, one-off payments, invoices and webhooks with familiar Cashier-style syntax, no boilerplate, and Mollie's simple, transparent API.
1. Install and configure
composer require mollie/laravel-cashier-mollie
php artisan vendor:publish --provider="Mollie\Laravel\Cashier\CashierServiceProvider"
Set the API key (a test key starts with test_):
MOLLIE_KEY=test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Add the Billable trait to whatever model pays you (usually User):
use Mollie\Laravel\Cashier\Billable;
class User extends Authenticatable
{
use Billable;
}
Publish and run the package migrations — they add mollie_customer_id, mollie_mandate_id, trial_ends_at and tax_percentage to users, plus subscriptions and order_items tables used for invoices.
php artisan vendor:publish --tag="cashier-migrations"
php artisan migrate
2. Register the webhook. Mollie is asynchronous: subscription renewals, mandate creation and payment status changes only become real when Mollie calls you back. Point the webhook URL in the Mollie dashboard (Settings → Website profiles → Webhooks) at the package's route, by default /webhook/mollie (check config('cashier.webhook')). If the route lives behind auth middleware, subscriptions will silently never activate.
3. Look first at: config/cashier.php (key, currency, locale, webhook path, invoice settings), then the Billable trait source, then Mollie\Laravel\Cashier\Subscription. Do not pattern-match against the Stripe Cashier docs — the two packages share a naming style, not an API.
First use case: a single monthly plan. Create the plan in the Mollie dashboard, then:
$checkout = $user->newSubscription('main', 'monthly-10-1.00')
->create($request->input('payment_method')); // e.g. 'creditcard', 'directdebit', 'ideal'
return $checkout; // RedirectResponse to Mollie's checkout
The user pays, Mollie creates a mandate, the webhook fires, and $user->subscribed('main') becomes true.
Recurring vs one-off payment methods. This is the single most important modelling decision. Mollie subscriptions are mandate-based: the first payment must use a method that supports recurring charges (credit card, SEPA Direct Debit, PayPal, and a few others). iDEAL, Bancontact, Klarna, etc. are one-off only — you can take a payment with them but you cannot attach a subscription. Offer recurring methods on your subscription page and one-off methods on your top-up/store page.
Subscription lifecycle. Treat the local subscriptions table as a cache of Mollie state, updated by webhooks:
if ($user->subscribed('main')) { ... }
if ($user->subscription('main')->onTrial()) { ... }
$user->subscription('main')->cancel(); // at period end
$user->subscription('main')->cancelNow();
$user->subscription('main')->resume();
$user->subscription('main')->swap('monthly-20-1.00');
Guard your own gating logic with subscribed() / onTrial() rather than reading the table directly, and let the webhook controller (Mollie\Laravel\Cashier\Http\Controllers\WebhookController) own the transitions.
One-off charges and invoices. For one-off amounts, use the billable model's charge/invoice helpers, and remember Mollie wants amounts as decimal strings with the right currency — let the package's formatter do it:
$user->charge(10.00, 'ideal', ['description' => 'Credits pack']);
$user->invoiceFor('Credits pack', 10.00);
$user->invoices() returns Mollie\Laravel\Cashier\Invoice objects; each invoice is backed by a Mollie order and its order_items rows. Expose a download route as GET /invoices/{id} returning $user->downloadInvoice($id) behind your own policy, and keep the route name in config so emails and the customer portal can link to it.
Hooking into webhooks. Override the package's webhook controller instead of writing your own endpoint — extend it and implement the handle... methods you care about, keeping the parent call:
class WebhookController extends \Mollie\Laravel\Cashier\Http\Controllers\WebhookController
{
protected function handlePaymentPaid(array $payload): Response
{
// top up credits, send receipts, notify Slack ...
return parent::handlePaymentPaid($payload);
}
}
Register the route to your subclass. The package dispatches events (WebhookReceived, WebhookHandled, plus subscription/payment events) which are often the cleanest integration point — listen for a paid payment and keep business logic out of the controller.
Trials and no-card signup. $user->newSubscription('main', 'monthly-10-1.00')->trialDays(14)->create() with no payment method works because no first payment is needed; trial_ends_at on the user / subscription records the window. Convert before the trial ends by sending the user through the mandate flow, otherwise Mollie can't charge and the subscription stalls.
Multi-currency. Call Cashier::useCurrency('EUR') in a service provider (or per-request for localised pricing) before any charge. Changing currency mid-subscription will break renewals, since the plan in Mollie is fixed in the profile's settlement currency.
redirectUrl return; grant it in the webhook handler. Make the endpoint idempotent — Mollie retries, and the same payment payload can arrive more than once.2xx. A 419 or a 302 to /login from a middleware mistake is the number one cause of "subscriptions never activate".Cashier::formatAmount() / Cashier::usesCurrency() rather than hand-rolling number_format. Write a test that asserts the amount sent to Mollie.valet share / ngrok / Expose and set the webhook URL to the tunnel. Alternatively, write a small artisan command that fetches a payment ID from Mollie and dispatches the handler locally — far easier to debug than round-tripping through the dashboard.MOLLIE_KEY is read at boot by the singleton client. After editing .env, run php artisan config:clear; in production with config caching, config:cache after every key change. A stale test key in production is a classic.Cashier::createCustomer, ->invoices() signatures, ->applyCoupon() and webhook payload shapes differ. The package's own test suite is the best reference for exact method signatures — read it before assuming a Cashier Stripe method exists.Http::fake(['api.mollie.com/*' => Http::response($fixture)]), so subscription tests don't hit the network. Test the webhook by POSTing the payload your handler expects and asserting a 200.Billable trait (override mollieCustomer() or the customer-creation hook to add metadata), the cashier config file, and the webhook controller subclass. For anything the package doesn't model — coupons, metered usage, proration — you're on your own; keep that logic in your app and reconcile against Mollie's subscription/discount primitives rather than forking the package.composer.json constraint against your Laravel and PHP versions before upgrading, and run a webhook replay in staging after each major bump.How can I help you explore Laravel packages today?