Start or resume Stripe Connect onboarding (admin)
/api/organization/stripeReturns a single-use hosted onboarding URL to redirect the operator to. Minted fresh on every call rather than stored, because a cached link 404s the second time it is clicked.
Idempotent in the way that matters: an org that already has an account gets a link to that account, and country is not read at all on that path. Two connected accounts for one venue is not a state this app could later untangle; the second would hold real charges.
Account creation uses the Stripe v2 API (stripe.v2.core.accounts). The v1 accounts.create({ type: "express" }) path is hard blocked on this platform account and fails in a way that reads like a permissions problem; v2 is also Stripe's current recommendation for new integrations. Accounts are created with dashboard: "full" and fees/losses collected by Stripe, which makes each venue the merchant of record for its own bookings; guest money settles in their balance and never passes through the platform, and no application fee is taken.
country is asked for rather than derived. A connected account's country is permanent and decides which payout rails and legal terms apply, and book_companies.country holds a display name written by the onboarding wizard, not an ISO code; guessing it from the currency would put a Dublin restaurant charging EUR into France.
Authorization
sessionCookie The dashboard's Supabase Auth session cookie, set at sign-in. Large sessions are split across
numbered chunks (…auth-token.0, .1), so treat this as a cookie family rather than one name.
Every request re-validates it against the Auth server (getUser()), never by decoding the cookie
locally: a JWT nothing has checked is not a credential. Tenancy is then read from the verified
app_metadata.company_id claim and enforced by row-level security; it is never read from request
input, on any route, ever.
role (admin / staff) is deliberately not in RLS. It gates specific actions in route code,
the operations marked admin below, so hiding a button in the UI is cosmetic only, and a route's
own check is the enforcement.
In: cookie
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/api/organization/stripe" \ -H "Content-Type: application/json" \ -d '{}'{ "url": "http://example.com"}Read this org's Stripe Connect status (admin) GET
Whether the org can take guest deposits (migration 0012), in three states rather than two, and the distinction is the point. `connected` means a Connect account exists; `chargesEnabled` means Stripe has actually cleared it to take card payments. **They are not the same, and assuming they are is the failure 0012's own header warns about**: a PaymentIntent is created successfully on a restricted account and only fails when the guest tries to pay it, which would strand a booking half-made. Calls Stripe on every request to re-read the capability status, because it flips when a verification completes; hours or days after the operator left onboarding, and emits no event this app subscribes to. A Stripe outage fails soft: the stored value is returned instead, since a settings page that errors because Stripe is slow is worse than one showing a stale answer. `configured: false` means the deployment has no Stripe keys at all (every preview branch until someone sets them). That is answered 200, not as an error; there is nothing wrong with the request. Reads `book_billing`, which has RLS with no `authenticated` policy, so this goes through the service-role client. There is no tenant-scoped path to these columns by design: it is what stops a member PATCHing themselves a payout destination through PostgREST.
Re-sync billing from Stripe, then redirect to Organization settings GET
Where a tier checkout, an add-on checkout, AND the Stripe Billing Portal all send an admin back, before they ever reach `/organization`. **Never grants anything itself**: `syncCompanyBilling()` (src/lib/billing/sync.ts) only re-reads what the webhook already decided, a beat earlier than the webhook might otherwise get to it, so an admin who just paid (or just cancelled in the portal) does not land on a page still showing the state from before they clicked. The webhook (`handleTierBought`/`handleAddonBought`/etc.) remains the only thing that actually writes an entitlement. **Fails open on every edge on purpose**: no session, a company with no Stripe customer yet, or the sync call itself throwing all fall through to the same redirect rather than stranding an admin on an error page after they have already paid. `TierCard`/`AddonBar`'s own delayed re-read after the redirect is the second safety net if even this proves too early; the webhook can genuinely still be in flight.