Concorbit HelpAll guides →

Stripe Connect Setup

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

  1. Open Settings, Payments.

  2. Click Connect with Stripe. You are redirected to Stripe's authorisation page.

  3. Log in to (or create) your Stripe account.

  4. Authorise concorbit to connect to the account.

  5. 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_id is set.

  • Stripe reports charges_enabled=true for the account.

  • The connected account has completed Stripe's onboarding (not in restricted or restricted_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:

  1. 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.

  2. savePaymentMethod on the server. The frontend POSTs the confirmed PaymentMethod id back together with the connected account id it saved against. The server checks:

    • canAcceptPayments is still true (else BillingConnectionInactiveException).

    • 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 (else BillingConnectionChangedException, 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_id is NULL or does not match the tenant's current stripe_connect_account_id is an orphan. Charging an orphan is refused.

  • Refusing stamps the invoice with autopay_blocked_pm_id so the recovery path knows which PM blocked it.

  • When the contact later saves a new card on the current connected account, OrphanRecoveryService::recoverForContact clears 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.account is 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.