Console API
Billing
11 endpoints.
/api/v1/admin/billing/{business_id}/auto-reloadGet a business's auto-reload config
Read auto-reload settings + spent-this-month / failure counters. Payments returns a default disabled config when unset; a payments outage degrades to 503.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
business_idrequired | path | string (uuid) | |
authorization | header | string | null |
Responses
200Successful Responseapplication/jsonSuccessResponse_AutoReloadData_| Field | Type | Description | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
request_idrequired | string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
success | true | default true | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
datarequired | AutoReloadData | AutoReloadData fields
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
metadata | ResponseMetadata | null | ResponseMetadata fields
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
meta | ResponseMeta | null | ResponseMeta fields
|
400Bad request401Unauthorized403Forbidden404Not found422Validation error500Internal server error503Service unavailable
Error bodies: ErrorResponse. See Errors.
/api/v1/admin/billing/{business_id}/auto-reloadSave a business's auto-reload config
Save auto-reload config. Real membership required — this standing authority charges the business's card unattended, so it is not something support sets on their behalf. A payments business 4xx is surfaced verbatim (``_api_error``); transport / 5xx → 503.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
business_idrequired | path | string (uuid) | |
authorization | header | string | null |
Request bodyrequired
application/jsonAutoReloadUpdateBody| Field | Type | Description |
|---|---|---|
enabledrequired | boolean | |
reload_amount | integer | null | |
trigger_credits | string | null | |
monthly_cap_amount | integer | null |
Responses
200Successful Responseapplication/jsonSuccessResponse_AutoReloadData_| Field | Type | Description |
|---|---|---|
request_idrequired | string | |
success | true | default true |
datarequired | AutoReloadData | AutoReloadData fieldsAutoReloadData, expanded above. |
metadata | ResponseMetadata | null | ResponseMetadata fieldsResponseMetadata, expanded above. |
meta | ResponseMeta | null | ResponseMeta fieldsResponseMeta, expanded above. |
400Bad request401Unauthorized403Forbidden404Not found422Validation error500Internal server error503Service unavailable
Error bodies: ErrorResponse. See Errors.
/api/v1/admin/billing/{business_id}/balanceGet a business's credit balance (used-of-total)
Credit balance for the used-of-total bar. The console aggregates the lots (Σ usage / Σ amount); no per-lot breakdown is rendered in A3.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
business_idrequired | path | string (uuid) | |
authorization | header | string | null |
Responses
200Successful Responseapplication/jsonSuccessResponse_BillingBalanceData_| Field | Type | Description | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
request_idrequired | string | |||||||||||||
success | true | default true | ||||||||||||
datarequired | BillingBalanceData | BillingBalanceData fields
| ||||||||||||
metadata | ResponseMetadata | null | ResponseMetadata fieldsResponseMetadata, expanded above. | ||||||||||||
meta | ResponseMeta | null | ResponseMeta fieldsResponseMeta, expanded above. |
400Bad request401Unauthorized403Forbidden404Not found422Validation error500Internal server error503Service unavailable
Error bodies: ErrorResponse. See Errors.
/api/v1/admin/billing/{business_id}/checkout-sessionMint a subscription Checkout session URL
Mint a Stripe Checkout URL to subscribe. Gated at ``user`` (a business write — excludes read-only ``viewer`` members; ``manager``/``developer``/``admin`` are Keystone global staff). No subscription gate, so a not-subscribed member can actually subscribe. ``email``/``name`` label the Stripe customer.
**Real membership required** (``staff_bypass=False``, 2026-09-03) — staff do *not* bypass it here as they do elsewhere. This is the mint that creates the business's Stripe customer, once, from ``body.email``; a staff-initiated subscribe would pin a Keystone address as the customer's permanent billing contact and divert every receipt and dunning email to it.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
business_idrequired | path | string (uuid) | |
authorization | header | string | null |
Request bodyrequired
application/jsonCheckoutSessionBody| Field | Type | Description |
|---|---|---|
plan_namerequired | string | |
emailrequired | string (email) | |
name | string | null | |
success_urlrequired | string | |
cancel_urlrequired | string | |
promotion_code_id | string (uuid) | null |
Responses
200Successful Responseapplication/jsonSuccessResponse_CheckoutSessionData_| Field | Type | Description | ||||||
|---|---|---|---|---|---|---|---|---|
request_idrequired | string | |||||||
success | true | default true | ||||||
datarequired | CheckoutSessionData | CheckoutSessionData fields
| ||||||
metadata | ResponseMetadata | null | ResponseMetadata fieldsResponseMetadata, expanded above. | ||||||
meta | ResponseMeta | null | ResponseMeta fieldsResponseMeta, expanded above. |
400Bad request401Unauthorized403Forbidden404Not found422Validation error500Internal server error503Service unavailable
Error bodies: ErrorResponse. See Errors.
/api/v1/admin/billing/{business_id}/grantsGrant credits to a subscribed business (platform admin)
Grant credits to a business that has a subscription (credit-grants-admin-design.md G2, G9–G11).
"Subscribed" means *has a subscription* — `active`, `grace`, `suspended` or `canceled` — not *is currently spendable*: a make-good to a lapsed customer is the case support hits most, and their credits do not vanish when they lapse. payments itself will grant to any id, so this is BFF policy, like `onboard-free`'s gate. The check runs **before** payments is called, so a refusal costs one read.
`retryable` (payments unreadable) answers **503**, and so does `status == "unknown"` on its own: it travels with `retryable` on every unreadable path but one (a 200 whose body lacks `status` is defaulted to `unknown` without it), and neither may fall through to a grant. The two checks are independent — an unreadable status is never spelled `none`, so their order does not matter (verified by mutation, 2026-09-21); what matters is that `unknown` is named explicitly. Then `none` → **409 `NOT_SUBSCRIBED`** — 409, not 402: the console reads 402 as "pay to proceed", the wrong instruction for staff acting on someone else's account.
No sor-side business lookup, unlike `onboard-free`: a business sor does not know has no billing account, reads as `none`, and is refused here — and payments cannot orphan anything because it refuses without an account. `actor` is derived from the JWT; `source` is always `manual`; no `price_book_id` is sent (G4).
payments' business answers pass through verbatim with `details`: **409 `GRANT_AMOUNT_MISMATCH`** (the same key with a different amount — `details.lot_id` / `details.granted`), **400 `NO_PRICE_BOOK_FOR_GRANT`** (a canceled business whose lots have all expired), **400 `VALIDATION_ERROR`**.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
business_idrequired | path | string (uuid) | |
authorization | header | string | null |
Request bodyrequired
application/jsonGrantBody| Field | Type | Description |
|---|---|---|
creditsrequired | string | min length 1 · max length 64 |
reasonrequired | string | min length 1 · max length 256 |
idempotency_keyrequired | string | min length 1 · max length 128 |
Responses
200Successful Responseapplication/jsonSuccessResponse_GrantData_| Field | Type | Description | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
request_idrequired | string | ||||||||||
success | true | default true | |||||||||
datarequired | GrantData | GrantData fields
| |||||||||
metadata | ResponseMetadata | null | ResponseMetadata fieldsResponseMetadata, expanded above. | |||||||||
meta | ResponseMeta | null | ResponseMeta fieldsResponseMeta, expanded above. |
400Bad request401Unauthorized403Forbidden404Not found422Validation error500Internal server error503Service unavailable
Error bodies: ErrorResponse. See Errors.
/api/v1/admin/billing/{business_id}/onboard-freePut a business on the $0 free plan (platform admin)
Create an `active` account and a `$0` subscription for a business that has never been billed — no Stripe object, no credit grant, usage rates to 0.
`plan_name` says which free plan — the catalogue can carry more than one, so the card the admin pressed is the plan that gets attached. `actor` is *not* in the body on purpose: it is derived from the caller's JWT, so the audit row on payments cannot be spoofed or left blank by whoever calls this.
The plan is validated by payments, not here: it owns the catalogue, and duplicating the `free`-kind check in the BFF would mean two places to keep right.
Payments' business answers pass through verbatim: **409** the business already has a billing account (naming the plan it is on) or has reached Stripe, **404** no such active plan, **400** the named plan is not a free one. Separately **404** when sor has no such business — payments has no `businesses` table and deliberately no foreign key to one, so it would happily onboard a typo'd id and leave an orphan billing account nothing would ever surface. sor owns that table; the check belongs here.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
business_idrequired | path | string (uuid) | |
authorization | header | string | null |
Request bodyrequired
application/jsonOnboardFreeBody| Field | Type | Description |
|---|---|---|
plan_namerequired | string | min length 1 · max length 256 |
Responses
200Successful Responseapplication/jsonSuccessResponse_OnboardFreeData_| Field | Type | Description | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
request_idrequired | string | |||||||||||||||||||
success | true | default true | ||||||||||||||||||
datarequired | OnboardFreeData | OnboardFreeData fields
| ||||||||||||||||||
metadata | ResponseMetadata | null | ResponseMetadata fieldsResponseMetadata, expanded above. | ||||||||||||||||||
meta | ResponseMeta | null | ResponseMeta fieldsResponseMeta, expanded above. |
400Bad request401Unauthorized403Forbidden404Not found422Validation error500Internal server error503Service unavailable
Error bodies: ErrorResponse. See Errors.
/api/v1/admin/billing/{business_id}/plansList the self-serve plan catalogue
The plan catalogue for the not-subscribed Billing screen (one card per plan). The catalogue is global; ``business_id`` scopes membership auth only (like the other billing reads). A payments outage degrades to 503 ``BILLING_UNAVAILABLE`` — the console just can't render cards.
**A platform admin also gets the `$0` free plan**, because only they can put a business on it (``POST …/onboard-free`` below). The flag is derived from the caller's role and is deliberately **not** a query parameter: the free plan has no Stripe price, so a customer who saw that card and pressed Subscribe would get a 503 — and it sorts first, being cheapest. Making it client-settable would put that failure one crafted request away.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
business_idrequired | path | string (uuid) | |
authorization | header | string | null |
Responses
200Successful Responseapplication/jsonSuccessResponse_PlanCatalogData_| Field | Type | Description | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
request_idrequired | string | ||||||||||
success | true | default true | |||||||||
datarequired | PlanCatalogData | PlanCatalogData fields
| |||||||||
metadata | ResponseMetadata | null | ResponseMetadata fieldsResponseMetadata, expanded above. | |||||||||
meta | ResponseMeta | null | ResponseMeta fieldsResponseMeta, expanded above. |
400Bad request401Unauthorized403Forbidden404Not found422Validation error500Internal server error503Service unavailable
Error bodies: ErrorResponse. See Errors.
/api/v1/admin/billing/{business_id}/portal-sessionMint a Customer Portal session URL
Mint a Stripe Customer Portal URL to manage the subscription / payment method. Gated at ``user`` (a business write) — same tier as subscribing, and the same real-membership requirement: the Portal lets the holder change the card and cancel, which is the business's call, not support's.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
business_idrequired | path | string (uuid) | |
authorization | header | string | null |
Request bodyrequired
application/jsonPortalSessionBody| Field | Type | Description |
|---|---|---|
return_urlrequired | string |
Responses
200Successful Responseapplication/jsonSuccessResponse_PortalSessionData_| Field | Type | Description | ||||||
|---|---|---|---|---|---|---|---|---|
request_idrequired | string | |||||||
success | true | default true | ||||||
datarequired | PortalSessionData | PortalSessionData fields
| ||||||
metadata | ResponseMetadata | null | ResponseMetadata fieldsResponseMetadata, expanded above. | ||||||
meta | ResponseMeta | null | ResponseMeta fieldsResponseMeta, expanded above. |
400Bad request401Unauthorized403Forbidden404Not found422Validation error500Internal server error503Service unavailable
Error bodies: ErrorResponse. See Errors.
/api/v1/admin/billing/{business_id}/reactivation-linkMint the reactivation payment link for a delinquent subscription
The hosted-invoice URL whose payment revives a grace/suspended subscription — payments resolves the sub's LITERAL latest invoice (finalizing it first when dormant cycles left it a draft; door 1, 2026-08). Payments' real 4xx (account not delinquent / latest invoice unpayable) passes through verbatim so the console can render it.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
business_idrequired | path | string (uuid) | |
authorization | header | string | null |
Responses
200Successful Responseapplication/jsonSuccessResponse_ReactivationLinkData_| Field | Type | Description | ||||||
|---|---|---|---|---|---|---|---|---|
request_idrequired | string | |||||||
success | true | default true | ||||||
datarequired | ReactivationLinkData | ReactivationLinkData fields
| ||||||
metadata | ResponseMetadata | null | ResponseMetadata fieldsResponseMetadata, expanded above. | ||||||
meta | ResponseMeta | null | ResponseMeta fieldsResponseMeta, expanded above. |
400Bad request401Unauthorized403Forbidden404Not found422Validation error500Internal server error503Service unavailable
Error bodies: ErrorResponse. See Errors.
/api/v1/admin/billing/{business_id}/recharge-sessionMint a one-time credit-recharge Checkout session URL
Mint a recharge Checkout URL (``amount`` = minor units; payments enforces $5–$10k + requires an in-force subscription). Real membership required — a support click here charges the customer's saved card for real. A payments **402** (no subscription) / **400** (bad amount) is surfaced verbatim; transport / 5xx → 503.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
business_idrequired | path | string (uuid) | |
authorization | header | string | null |
Request bodyrequired
application/jsonRechargeSessionBody| Field | Type | Description |
|---|---|---|
amountrequired | integer | > 0 |
success_urlrequired | string | |
cancel_urlrequired | string |
Responses
200Successful Responseapplication/jsonSuccessResponse_RechargeSessionData_| Field | Type | Description | ||||||
|---|---|---|---|---|---|---|---|---|
request_idrequired | string | |||||||
success | true | default true | ||||||
datarequired | RechargeSessionData | RechargeSessionData fields
| ||||||
metadata | ResponseMetadata | null | ResponseMetadata fieldsResponseMetadata, expanded above. | ||||||
meta | ResponseMeta | null | ResponseMeta fieldsResponseMeta, expanded above. |
400Bad request401Unauthorized403Forbidden404Not found422Validation error500Internal server error503Service unavailable
Error bodies: ErrorResponse. See Errors.
/api/v1/admin/billing/{business_id}/statusGet a business's billing/entitlement status
Read plan + entitlement for the Billing overview. ``get_access_status`` is fail-closed for the enforcement gate, but for *display* an unreadable status must not masquerade as suspended — so a ``retryable`` (payments-unreachable) result surfaces as 503, not a fake status.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
business_idrequired | path | string (uuid) | |
authorization | header | string | null |
Responses
200Successful Responseapplication/jsonSuccessResponse_BillingStatusData_| Field | Type | Description | |||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
request_idrequired | string | ||||||||||||||||||||||||||||
success | true | default true | |||||||||||||||||||||||||||
datarequired | BillingStatusData | BillingStatusData fields
| |||||||||||||||||||||||||||
metadata | ResponseMetadata | null | ResponseMetadata fieldsResponseMetadata, expanded above. | |||||||||||||||||||||||||||
meta | ResponseMeta | null | ResponseMeta fieldsResponseMeta, expanded above. |
400Bad request401Unauthorized403Forbidden404Not found422Validation error500Internal server error503Service unavailable
Error bodies: ErrorResponse. See Errors.