Stripe Connect setup
concorbit uses Stripe Connect to accept payments on the MSP's Stripe account, not on a platform-pooled account. The money moves directly to the MSP; concorbit acts as the platform facilitator and never holds funds. This page covers the wiring, the readiness check (canAcceptPayments), and what happens when the connected account is rotated.
For the payment lifecycle on top of this, see Payments.
Connecting
Open Settings, Payments.
Click Connect with Stripe. You are redirected to Stripe's authorisation page.
Log in to (or create) your Stripe account.
Authorise concorbit to connect to the account.
You are redirected back to concorbit with a confirmation message.
Behind the scenes, the OAuth callback writes the connected account id to tenants.stripe_connect_account_id. Every subsequent Stripe call is built via BillingStripeClientFactory::forTenantBilling, which bakes stripe_account into the client; the platform key is never used to charge a tenant's customer.
canAcceptPayments: the readiness gate
A connected account is not the same as a ready account. concorbit gates every payment action on Tenant::canAcceptPayments(), which returns true only when all of:
stripe_connect_account_idis set.Stripe reports
charges_enabled=truefor the account.The connected account has completed Stripe's onboarding (not in
restrictedorrestricted_soon).
When canAcceptPayments is false, the following actions refuse cleanly with a typed exception (BillingConnectionInactiveException):
Save a payment method (
AutopayService::savePaymentMethod).Create a portal pay-now PaymentIntent.
Run an autopay charge.
Refund through the concorbit UI.
The refusal is fail-fast: the Stripe API is never called when the gate is closed, so there is no risk of a half-saved card on a disabled account.
Saving a payment method
The save flow is in two phases:
SetupIntent on the connected account. The portal or staff UI calls
createSetupIntent, which returns a client secret and the current connected account id. The frontend confirms the SetupIntent inline (Payment Element) on that account.savePaymentMethodon the server. The frontend POSTs the confirmed PaymentMethod id back together with the connected account id it saved against. The server checks:canAcceptPaymentsis still true (elseBillingConnectionInactiveException).The supplied connected account id is not empty (else
BillingConnectionContextMissingException, a stale frontend bundle).The supplied id matches the tenant's current
stripe_connect_account_id(elseBillingConnectionChangedException, a rotation race between setup and save).
Only when all three pass is the local PaymentMethod row written, with stripe_account_id stamped to the current connected account id. That stamp is what the orphan detector compares against on every future charge.
Connect account rotation
If the tenant disconnects and reconnects Stripe (or Stripe rotates the connected account id for some operational reason), every saved payment method now points at the old account. Charging them on the new account would fail or, worse, succeed against a stale customer. concorbit's protection (CB#5):
The autopay path runs an orphan guard before building the Stripe client. A PaymentMethod whose
stripe_account_idis NULL or does not match the tenant's currentstripe_connect_account_idis an orphan. Charging an orphan is refused.Refusing stamps the invoice with
autopay_blocked_pm_idso the recovery path knows which PM blocked it.When the contact later saves a new card on the current connected account,
OrphanRecoveryService::recoverForContactclears the blocks on their previously-blocked invoices and dispatches a per-invoice retry.
A new PM save also flips contacts.autopay_enabled = true so the customer does not have to take a separate action to re-enable autopay.
Backfilling legacy PMs
Legacy payment methods saved before the stripe_account_id column existed have a NULL stamp. To backfill them from the durable stripe_customers table:
php artisan concorbit:backfill-payment-method-accounts
The command is idempotent (only touches NULL stamps), chunks by id at 500 per chunk, and explicitly sets a per-tenant scope before each chunk so the HKDF-derived encryption key resolves to the right tenant. Re-runnable safely.
Disconnecting
Click Disconnect Stripe under Settings, Payments. The connection is deactivated immediately. After disconnecting:
Online payment links on invoices no longer work.
Autopay stops for every contact on the tenant.
Existing payment and payment-method records are retained (the orphan guard is what protects against charging them on a wrong account if you later reconnect to a different Stripe account).
You can reconnect at any time. If the new Stripe account is the same as before, saved PMs become usable again. If it is different, every saved PM is orphaned and the recovery flow kicks in as customers save new cards.
Webhook configuration
Stripe communicates payment events to concorbit via webhooks. These are configured automatically when you connect Stripe. The concorbit endpoint splits events into:
Connect events (events where
event.accountis set) route to the tenant ledger.Platform events route to concorbit's own platform billing.
Every event is durably deduplicated by Stripe event id, so a replay or duplicate delivery never writes twice. See Disputes for the charge.dispute.* event handling and Payments for charge.refunded.