Keystone Developers
Open Keystone
API reference

Console API

Billing

11 endpoints.

GET/api/v1/admin/billing/{business_id}/auto-reload

Get 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

NameInTypeDescription
business_idrequiredpathstring (uuid)
authorizationheaderstring | null

Responses

200Successful Response
application/jsonSuccessResponse_AutoReloadData_
FieldTypeDescription
request_idrequiredstring
successtruedefault true
datarequiredAutoReloadData
AutoReloadData fields
FieldTypeDescription
enabledrequiredboolean
reload_amountinteger | null
trigger_creditsstring | null
monthly_cap_amountinteger | null
spent_this_monthintegerdefault 0
consecutive_failuresintegerdefault 0
metadataResponseMetadata | null
ResponseMetadata fields
FieldTypeDescription
paginationPaginationMetadata | null
PaginationMetadata fields
FieldTypeDescription
limitrequiredinteger
next_cursorstring | null
prev_cursorstring | null
has_morerequiredboolean
has_prevbooleandefault false
total_countinteger | null
status_countsobject | null
object | null fields

Map of string to integer

lifecycle_countsLifecycleCounts | null
LifecycleCounts fields
FieldTypeDescription
leadintegerdefault 0
prospectintegerdefault 0
customerintegerdefault 0
former_customerintegerdefault 0
classification_countsClassificationCounts | null
ClassificationCounts fields
FieldTypeDescription
signalobject
object fields

Nested object (not expanded)

touchobject
object fields

Nested object (not expanded)

stageobject
object fields

Nested object (not expanded)

metaResponseMeta | null
ResponseMeta fields
FieldTypeDescription
totalinteger | null
total_countinteger | null
pageinteger | null
per_pageinteger | null
  • 400Bad request
  • 401Unauthorized
  • 403Forbidden
  • 404Not found
  • 422Validation error
  • 500Internal server error
  • 503Service unavailable

Error bodies: ErrorResponse. See Errors.

PUT/api/v1/admin/billing/{business_id}/auto-reload

Save 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

NameInTypeDescription
business_idrequiredpathstring (uuid)
authorizationheaderstring | null

Request bodyrequired

application/jsonAutoReloadUpdateBody
FieldTypeDescription
enabledrequiredboolean
reload_amountinteger | null
trigger_creditsstring | null
monthly_cap_amountinteger | null

Responses

200Successful Response
application/jsonSuccessResponse_AutoReloadData_
FieldTypeDescription
request_idrequiredstring
successtruedefault true
datarequiredAutoReloadData
AutoReloadData fields

AutoReloadData, expanded above.

metadataResponseMetadata | null
ResponseMetadata fields

ResponseMetadata, expanded above.

metaResponseMeta | null
ResponseMeta fields

ResponseMeta, expanded above.

  • 400Bad request
  • 401Unauthorized
  • 403Forbidden
  • 404Not found
  • 422Validation error
  • 500Internal server error
  • 503Service unavailable

Error bodies: ErrorResponse. See Errors.

GET/api/v1/admin/billing/{business_id}/balance

Get 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

NameInTypeDescription
business_idrequiredpathstring (uuid)
authorizationheaderstring | null

Responses

200Successful Response
application/jsonSuccessResponse_BillingBalanceData_
FieldTypeDescription
request_idrequiredstring
successtruedefault true
datarequiredBillingBalanceData
BillingBalanceData fields
FieldTypeDescription
spendable_creditsrequiredstring
debt_creditsrequiredstring
lotsrequiredBillingBalanceLot[]
BillingBalanceLot[] fields

BillingBalanceLot (not expanded)

metadataResponseMetadata | null
ResponseMetadata fields

ResponseMetadata, expanded above.

metaResponseMeta | null
ResponseMeta fields

ResponseMeta, expanded above.

  • 400Bad request
  • 401Unauthorized
  • 403Forbidden
  • 404Not found
  • 422Validation error
  • 500Internal server error
  • 503Service unavailable

Error bodies: ErrorResponse. See Errors.

POST/api/v1/admin/billing/{business_id}/checkout-session

Mint 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

NameInTypeDescription
business_idrequiredpathstring (uuid)
authorizationheaderstring | null

Request bodyrequired

application/jsonCheckoutSessionBody
FieldTypeDescription
plan_namerequiredstring
emailrequiredstring (email)
namestring | null
success_urlrequiredstring
cancel_urlrequiredstring
promotion_code_idstring (uuid) | null

Responses

200Successful Response
application/jsonSuccessResponse_CheckoutSessionData_
FieldTypeDescription
request_idrequiredstring
successtruedefault true
datarequiredCheckoutSessionData
CheckoutSessionData fields
FieldTypeDescription
checkout_urlrequiredstring
metadataResponseMetadata | null
ResponseMetadata fields

ResponseMetadata, expanded above.

metaResponseMeta | null
ResponseMeta fields

ResponseMeta, expanded above.

  • 400Bad request
  • 401Unauthorized
  • 403Forbidden
  • 404Not found
  • 422Validation error
  • 500Internal server error
  • 503Service unavailable

Error bodies: ErrorResponse. See Errors.

POST/api/v1/admin/billing/{business_id}/grants

Grant 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

NameInTypeDescription
business_idrequiredpathstring (uuid)
authorizationheaderstring | null

Request bodyrequired

application/jsonGrantBody
FieldTypeDescription
creditsrequiredstringmin length 1 · max length 64
reasonrequiredstringmin length 1 · max length 256
idempotency_keyrequiredstringmin length 1 · max length 128

Responses

200Successful Response
application/jsonSuccessResponse_GrantData_
FieldTypeDescription
request_idrequiredstring
successtruedefault true
datarequiredGrantData
GrantData fields
FieldTypeDescription
lot_idrequiredstring (uuid)
grantedrequiredstring
metadataResponseMetadata | null
ResponseMetadata fields

ResponseMetadata, expanded above.

metaResponseMeta | null
ResponseMeta fields

ResponseMeta, expanded above.

  • 400Bad request
  • 401Unauthorized
  • 403Forbidden
  • 404Not found
  • 422Validation error
  • 500Internal server error
  • 503Service unavailable

Error bodies: ErrorResponse. See Errors.

POST/api/v1/admin/billing/{business_id}/onboard-free

Put 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

NameInTypeDescription
business_idrequiredpathstring (uuid)
authorizationheaderstring | null

Request bodyrequired

application/jsonOnboardFreeBody
FieldTypeDescription
plan_namerequiredstringmin length 1 · max length 256

Responses

200Successful Response
application/jsonSuccessResponse_OnboardFreeData_
FieldTypeDescription
request_idrequiredstring
successtruedefault true
datarequiredOnboardFreeData
OnboardFreeData fields
FieldTypeDescription
business_idrequiredstring (uuid)
statusrequiredstring
plan_namerequiredstring
plan_kindrequiredstring
subscription_idrequiredstring (uuid)
metadataResponseMetadata | null
ResponseMetadata fields

ResponseMetadata, expanded above.

metaResponseMeta | null
ResponseMeta fields

ResponseMeta, expanded above.

  • 400Bad request
  • 401Unauthorized
  • 403Forbidden
  • 404Not found
  • 422Validation error
  • 500Internal server error
  • 503Service unavailable

Error bodies: ErrorResponse. See Errors.

GET/api/v1/admin/billing/{business_id}/plans

List 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

NameInTypeDescription
business_idrequiredpathstring (uuid)
authorizationheaderstring | null

Responses

200Successful Response
application/jsonSuccessResponse_PlanCatalogData_
FieldTypeDescription
request_idrequiredstring
successtruedefault true
datarequiredPlanCatalogData
PlanCatalogData fields
FieldTypeDescription
plansrequiredPlanCatalogItem[]
PlanCatalogItem[] fields

PlanCatalogItem (not expanded)

promotionsPromotionOfferData[]default []
PromotionOfferData[] fields

PromotionOfferData (not expanded)

metadataResponseMetadata | null
ResponseMetadata fields

ResponseMetadata, expanded above.

metaResponseMeta | null
ResponseMeta fields

ResponseMeta, expanded above.

  • 400Bad request
  • 401Unauthorized
  • 403Forbidden
  • 404Not found
  • 422Validation error
  • 500Internal server error
  • 503Service unavailable

Error bodies: ErrorResponse. See Errors.

POST/api/v1/admin/billing/{business_id}/portal-session

Mint 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

NameInTypeDescription
business_idrequiredpathstring (uuid)
authorizationheaderstring | null

Request bodyrequired

application/jsonPortalSessionBody
FieldTypeDescription
return_urlrequiredstring

Responses

200Successful Response
application/jsonSuccessResponse_PortalSessionData_
FieldTypeDescription
request_idrequiredstring
successtruedefault true
datarequiredPortalSessionData
PortalSessionData fields
FieldTypeDescription
portal_urlrequiredstring
metadataResponseMetadata | null
ResponseMetadata fields

ResponseMetadata, expanded above.

metaResponseMeta | null
ResponseMeta fields

ResponseMeta, expanded above.

  • 400Bad request
  • 401Unauthorized
  • 403Forbidden
  • 404Not found
  • 422Validation error
  • 500Internal server error
  • 503Service unavailable

Error bodies: ErrorResponse. See Errors.

POST/api/v1/admin/billing/{business_id}/reactivation-link

Mint 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

NameInTypeDescription
business_idrequiredpathstring (uuid)
authorizationheaderstring | null

Responses

200Successful Response
application/jsonSuccessResponse_ReactivationLinkData_
FieldTypeDescription
request_idrequiredstring
successtruedefault true
datarequiredReactivationLinkData
ReactivationLinkData fields
FieldTypeDescription
reactivation_urlrequiredstring
metadataResponseMetadata | null
ResponseMetadata fields

ResponseMetadata, expanded above.

metaResponseMeta | null
ResponseMeta fields

ResponseMeta, expanded above.

  • 400Bad request
  • 401Unauthorized
  • 403Forbidden
  • 404Not found
  • 422Validation error
  • 500Internal server error
  • 503Service unavailable

Error bodies: ErrorResponse. See Errors.

POST/api/v1/admin/billing/{business_id}/recharge-session

Mint 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

NameInTypeDescription
business_idrequiredpathstring (uuid)
authorizationheaderstring | null

Request bodyrequired

application/jsonRechargeSessionBody
FieldTypeDescription
amountrequiredinteger> 0
success_urlrequiredstring
cancel_urlrequiredstring

Responses

200Successful Response
application/jsonSuccessResponse_RechargeSessionData_
FieldTypeDescription
request_idrequiredstring
successtruedefault true
datarequiredRechargeSessionData
RechargeSessionData fields
FieldTypeDescription
checkout_urlrequiredstring
metadataResponseMetadata | null
ResponseMetadata fields

ResponseMetadata, expanded above.

metaResponseMeta | null
ResponseMeta fields

ResponseMeta, expanded above.

  • 400Bad request
  • 401Unauthorized
  • 403Forbidden
  • 404Not found
  • 422Validation error
  • 500Internal server error
  • 503Service unavailable

Error bodies: ErrorResponse. See Errors.

GET/api/v1/admin/billing/{business_id}/status

Get 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

NameInTypeDescription
business_idrequiredpathstring (uuid)
authorizationheaderstring | null

Responses

200Successful Response
application/jsonSuccessResponse_BillingStatusData_
FieldTypeDescription
request_idrequiredstring
successtruedefault true
datarequiredBillingStatusData
BillingStatusData fields
FieldTypeDescription
statusrequiredstring
accessrequiredboolean
ops_blockedbooleandefault false
plan_namestring | null
plan_kindstring | null
current_period_endstring | null
cancel_at_period_endbooleandefault false
grace_days_remaininginteger | null
metadataResponseMetadata | null
ResponseMetadata fields

ResponseMetadata, expanded above.

metaResponseMeta | null
ResponseMeta fields

ResponseMeta, expanded above.

  • 400Bad request
  • 401Unauthorized
  • 403Forbidden
  • 404Not found
  • 422Validation error
  • 500Internal server error
  • 503Service unavailable

Error bodies: ErrorResponse. See Errors.