Read Gaplessly's own billing details for this org (admin)
/api/organization/billingMigration 0128, the scoped-down native-billing build (docs/backlog.md): a SECOND, distinct ABN and a receipts-email override for GAPLESSLY'S OWN invoices to this venue, kept on book_billing rather than book_companies. Not the same field as receiptAbn on PATCH /api/companies (migration 0126), which is the venue's ABN on ITS OWN guest receipts; the two answer opposite questions and live on opposite tables on purpose.
Reads book_billing, which has RLS with no authenticated policy, so this goes through the service-role client, the same reason /api/organization/addons and /api/organization/tier do. Both fields default to null when the org has never set them, or has no book_billing row at all yet.
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/billing"{ "abn": "string", "receiptsEmail": "string"}Start tier checkout, or change a paid plan in place (admin) POST
Without a paid plan: returns a **Stripe-hosted Checkout URL** for the browser to navigate to, subscribing this business to the given tier (`annual: true` for the yearly price). Sibling to `POST /api/organization/addons`, and the same rule applies: this route never grants an entitlement; `book_billing.tier`/`subscription_status` are written only by the Stripe webhook (`handleTierBought`, `handleTierChanged`), because the browser arriving at a success URL is the party that benefits from lying about having paid. **With a paid plan** (migration 0058's `tier_subscription_id`, active or trialing): changes that subscription in place (`changeTierPlan`), never a second one. Only the tier item's price changes, charged or credited at once, and a change that needs paying happens only once paid: `url` is then the invoice to pay. Stripe's portal cannot do this, since it never switches a subscription with a metered price or more than one product. Asking for the plan it is already on, or for Solo (Venue Starter) (reached by cancelling), is a 400. Admin-only: changing what the organization is billed is not a staff decision. Not engine-gated; every business type has a ladder.
Save Gaplessly's own billing details for this org (admin) PATCH
Only the keys actually sent are touched: either field, or both, in one call. `abn` is normalised the same way `receiptAbn` is on `PATCH /api/companies` (`normalizeAbn`, `lib/billing/receipt-format.ts`): spaces and hyphens stripped, then must be exactly 11 digits or empty. `receiptsEmail` follows the same email-shape check every other admin-settings route in this file uses. An empty string on either field CLEARS it to null rather than being ignored. Upserts into `book_billing` without ever naming `tier`/`subscription_status`/`stripe_customer_id` in the payload, the same trap `ensureCustomer`'s own comment (`lib/billing/subscription.ts`) names for every other upsert into this table: naming those columns in an upsert would reset a paying org. Admin-only: this is a fact about what the organization is billed, the same class of act as a tier or add-on change.