expdev07/laravel-cashier-stripe-connect
Laravel Cashier Stripe Connect extends Cashier with Stripe Connect support, letting platforms manage connected accounts, charge customers, and handle payouts and transfers—ideal for marketplaces, SaaS platforms, and multi-vendor apps built on Laravel.
This package layers Stripe Connect semantics onto Cashier, so the fastest path is to already have Cashier working for a single-account setup, then add the Connect dimension.
Prerequisites
laravel/cashier install with a working billable model and webhook endpoint.Install and wire up
composer require expdev07/laravel-cashier-stripe-connect
Because the package is Cashier-adjacent, expect to publish a config file and possibly migrations:
php artisan vendor:publish --provider="Expdev07\CashierConnect\CashierConnectServiceProvider"
Confirm the exact provider namespace and published tags by reading the package's src/ directory and composer.json first — the README is thin and the package has had no release since 2022, so the source is the authoritative reference.
Where to look first, in order
composer.json — the illuminate/* and laravel/cashier constraints decide whether you can even install it on your framework version.src/ — which Cashier classes it extends or decorates (likely Billable, Subscription, and a payment/charge builder).stripe/connect key wiring.First use case: a destination charge with a platform fee
The canonical marketplace flow is: a buyer pays your platform, funds are routed to the seller's connected account, and your platform keeps an application fee. In Cashier-Connect terms this is roughly:
// Verify exact method names against src/ before shipping.
$user->charge($amountInCents, $paymentMethod, [
'destination' => $seller->stripe_account_id,
'application_fee_amount' => (int) round($amountInCents * 0.10),
]);
Set this up in test mode first, confirm the transfer and fee appear on both the platform and connected-account sides of the Stripe dashboard, then build the UI around connected-account onboarding (account_links) before worrying about subscriptions.
Connected-account onboarding is your first real feature, not charging. Stripe requires each seller to complete KYC via a hosted onboarding flow (AccountLink) before they can receive funds. Treat account creation and onboarding as a separate domain from billing:
StripeAccount (Express or Standard) per seller, store the acct_... ID on your users/sellers table.account_link on demand and redirect; never cache the URL (they expire in minutes).charges_enabled and payouts_enabled from the account object, not on "onboarding completed" flags you invent.Two charge architectures — pick one and standardize.
| Pattern | When to use | Trade-off |
|---|---|---|
| Destination charges (platform is merchant of record) | Simple marketplace; you set the price, want one dispute surface | Disputes and refunds land on the platform, not the seller |
| Separate charges and transfers | Sellers set their own prices; you need to decouple charge from payout | Two API objects to reconcile, transfer timing is your problem |
Wrap whichever you choose in a single application-level service so the rest of the codebase never calls Stripe directly:
class MarketplaceCharge
{
public function __construct(private FeeCalculator $fees) {}
public function charge(Billable $buyer, Billable $seller, int $amount, string $pm): Payment
{
$fee = $this->fees->for($seller, $amount);
return $buyer->charge($amount, $pm, [
'destination' => $seller->stripe_account_id,
'application_fee_amount' => $fee,
]);
}
}
Application fees belong behind a calculator, not inline * 0.10. Fees change (tiered rates, per-category, minimums, currency-specific rounding). A FeeCalculator object keeps the policy testable and lets you introduce per-seller overrides without touching charge sites.
Subscriptions need a decision up front. Cashier's subscription lifecycle is built around a single merchant. On a marketplace you are usually subscribing the buyer to the platform while routing revenue to sellers — or you are running subscriptions on behalf of a connected account (stripe_account header on subscription creation). Confirm which the package supports by reading its Subscription implementation; the two are not interchangeable, and invoices, proration, and webhooks differ.
Webhooks are where reliability lives. You need two streams:
invoice.paid, charge.refunded, customer.subscription.updated) at your normal Cashier endpoint.account.updated, transfer.created, payout.paid, charge.dispute.created) — these must be routed with the connected-account context so you can update the right seller's state.// routes/api.php — separate endpoints, separate secrets if configured.
Route::post('/stripe/webhook', StripeWebhookController::class);
Route::post('/stripe/connect/webhook', StripeConnectWebhookController::class);
Reconcile, don't assume. Persist every charge, fee, and transfer to your own tables with the Stripe IDs. Your database is what your finance team queries; Stripe is what you audit against. A payments table with stripe_charge_id, stripe_transfer_id, application_fee_amount, and connected_account_id will save you weeks at quarter-end.
Integration tip: build the "seller dashboard" view from webhook state, not API polling. Cache charges_enabled, balance, and payout status on the seller record, updated by account.updated and payout.* events. Polling the Stripe API per page load will rate-limit you at scale.
The package is frozen at 2022. Last release was April 2022 (Laravel 9). Confirm Laravel 10/11 support before assuming it installs, and expect the composer.json constraints — not a runtime error — to be your first blocker. Pin the version explicitly ("expdev07/laravel-cashier-stripe-connect": "1.2.0") so a future patch cannot silently change behavior.
The documentation is sparse. Method names in this assessment are illustrative. Before writing application code, grep src/ for the actual public API — particularly whether charges are exposed as $billable->charge(...), a dedicated Payment builder, or a trait method. Budget an hour for a source read; it will save a day of debugging.
Platform vs connected account keys. Almost every confusing 404/No such charge in Connect work is a request made with the platform key that should have used the Stripe-Account header (or vice versa). Cashier-Connect is supposed to handle this for you — verify it does by watching the Stripe-Account header in a test-mode request.
Application-fee arithmetic is in the smallest currency unit and currency-specific. Don't round with floats. Use integer minor units end-to-end and confirm your fee calculator handles zero-decimal currencies (JPY, KRW) and Stripe's rounding rules. A one-cent fee mismatch on refunds causes real reconciliation headaches.
Refunds and disputes need explicit handling. In destination-charge mode, refunding a charge by default does not claw back the application fee or the transfer. Set refund_application_fee and reverse_transfer deliberately — the defaults will surprise you.
Webhook signatures differ per endpoint. Connect webhook secrets are not the same as your platform endpoint secret. Read config/cashier.php and the package config carefully; a misconfigured secret fails silently if you swallow exceptions.
Idempotency is on you. Pass an Idempotency-Key (e.g., your order UUID) on every create call. Retries on timeouts are common, and duplicate charges are the failure mode that loses customers.
Test-mode friction. Connect testing needs multiple test accounts and, for Express onboarding, stripe listen --forward-connect-to. If your local environment already forwards platform events, add a second forwarder for Connect — otherwise account.updated never arrives and sellers stay stuck in "pending."
Escape hatch. For edge cases the package doesn't cover (treasury, multi-currency payouts, complex fee splits), you can always drop to Stripe\StripeClient for that specific operation and keep Cashier-Connect for the CRUD-heavy parts. Don't fight the abstraction; you are not on the maintainer's support contract.
Ownership. Assume a single-maintainer, community project with no SLA. Name an internal owner, keep a quarterly check on framework compatibility, and pre-identify whether the alternative is raw stripe/stripe-php plus a thin internal wrapper. That decision will eventually be forced; better to have it scoped than to discover it mid-upgrade.
How can I help you explore Laravel packages today?