Read this org's Stripe Connect status (admin)
/api/organization/stripeWhether 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.
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
Response Body
application/json
application/json
application/json
curl -X GET "https://example.com/api/organization/stripe"{ "configured": true, "connected": true, "chargesEnabled": true, "accountId": "string"}Read this business's metered usage for the current billing period (admin) GET
What `UsageCard` renders on `/billing`. A read of the append-only `book_usage_events` ledger via `lib/usage.ts`. **Two metered resources since pass 2 (2026-08-08), and they are not symmetric.** `sms` is gated and re-billed: every `sendSms()` call site goes through `meteredSendSms`, and each segment is billed at A$0.09 per text. `email` is neither: every `meteredSendEmail` send is counted, but email is included on every plan, is not re-billed, and cannot be capped (see that function's own header). Bookings remain the one documented chokepoint still uninstrumented. **No cap or block lives here or anywhere else** (2026-08-07): `sms` is either off entirely (a plan without texts, or a trialing tier that hasn't converted to paid, per `planAllows`'s own trial gate) or fully on with no allowance and no ceiling. This route only ever reports what was sent, never a reason a send was refused for running out of anything. `allowed: false` zeroes `usedThisPeriod` without reading the ledger for SMS, because there is nothing to meter for a business that cannot use the feature. **It no longer short-circuits the whole response**: pass 1 did, which would now hide the email count from every company whose plan has no texts, i.e. most of them. The four top-level fields keep their pass-1 shape and meaning exactly and describe SMS alone; `email` was added as a nested object rather than a reshuffle, so this stayed an additive change. `included` is always `0` (no tier or add-on bundles a free allowance any more, see `TIER_LIMITS`'s own comment), kept in the response rather than dropped so `UsageCard` never has to special-case its absence. One billing-period boundary serves both resources, derived from `book_billing.current_period_end` via `periodStartFor()`.
Start or resume Stripe Connect onboarding (admin) POST
Returns 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.