bus-api-provider-billing — Billing API provider
bus-api-provider-billing — Billing API provider
bus-api-provider-billing serves public end-user billing APIs. It validates Bus
API JWTs, derives account_id from JWT sub, and delegates billing state and
setup work to integrations. It does not contain Stripe-specific code.
Public endpoints are caller-owned only. The provider derives account identity
from JWT sub and rejects caller-supplied account metadata. Internal catalog,
cross-account status, and entitlement-check endpoints require the internal
audience with narrow billing scopes. Error responses redact bearer tokens,
Stripe secret keys, and PostgreSQL passwords.
Production deployments should configure PostgreSQL catalog storage with
BUS_BILLING_DATABASE_URL. The provider creates a minimal catalog schema when
it is missing. Without a database URL, internal catalog endpoints return a
deterministic storage-unavailable error instead of silently using volatile
storage.
Public Authentication
Public endpoints require an end-user API JWT with audience ai.hg.fi/api.
The account is always derived from JWT sub.
Send the token as Authorization: Bearer <jwt> on every public request.
Internal Authentication
Internal endpoints require audience ai.hg.fi/internal and narrow billing
scopes. End-user API tokens are rejected.
Send the internal token as Authorization: Bearer <jwt> on every internal
request.
GET /api/v1/billing/status
Returns billing state for the authenticated account.
The response can include enabled features, current quota usage, setup guidance,
and upgrade guidance. Requires billing:read.
POST /api/v1/billing/checkout-session
Creates a hosted billing setup URL for the authenticated account.
The URL may point to Stripe Checkout or another configured provider. Requires
billing:setup.
Send Content-Type: application/json with either {} or
{"feature":"llm:proxy","return_url":"https://app.example.test/billing/return"}.
Success returns 200 OK with {"url":"https://...","provider":"stripe"}.
POST /api/v1/billing/portal-session
Creates a hosted billing portal URL for the authenticated account.
Users manage payment methods, invoices, and subscriptions in the provider
portal. Requires billing:read.
Send Content-Type: application/json with {} or
{"return_url":"https://app.example.test/billing/return"}.
Success returns 200 OK with {"url":"https://...","provider":"stripe"}.
GET /api/internal/billing/catalog
Returns the active provider-neutral billing catalog.
Requires billing:catalog:read with the internal audience.
PUT /api/internal/billing/catalog
Replaces the active provider-neutral billing catalog.
Requires billing:catalog:write with the internal audience.
The catalog describes products, plans, prices, meters, feature scopes, quota
rules, and optional non-secret provider mappings.
Send JSON with top-level products, plans, and meters arrays. Product and
plan IDs must be stable strings, prices use integer minor units, and quota
limits must be positive integers. A minimal accepted catalog is:
{
"products": [{"id": "llm", "name": "LLM access"}],
"meters": [{"name": "bus_llm_tokens", "unit": "tokens"}],
"plans": [{
"id": "llm-basic",
"product_id": "llm",
"features": ["llm:proxy"],
"prices": [{"currency": "eur", "unit_amount": 1000, "interval": "month"}],
"quotas": [{"feature": "llm:proxy", "meter_event_name": "bus_llm_tokens", "window": "month", "limit": 1000000}]
}]
}
Success returns 200 OK with the stored catalog or update status. Invalid
catalog JSON returns 400, missing internal audience or
billing:catalog:write returns 401 or 403, and unavailable catalog storage
returns 503.
GET /api/internal/billing/accounts/{account_id}/status
Returns billing status for an operator-selected account.
Requires billing:read with the internal audience.
account_id must be the stable account UUID used as the API token subject.
Success returns the same billing status shape as the public status endpoint for
that account. Invalid UUIDs return 400; missing internal authority returns
401 or 403.
POST /api/internal/billing/entitlement-check
Checks whether an account may use a paid feature such as llm:proxy or
container:run.
Requires billing:entitlement:check with the internal audience. LLM and
container providers call this before starting billable work.
Denied responses use stable reasons such as billing_required and
quota_exceeded, with user-facing guidance when available.
Use the stable account UUID from the auth provider or API token sub; for
example, send
{"account_id":"00000000-0000-4000-8000-000000000001","scope":"llm:proxy","usage":{"meter_event_name":"bus_llm_tokens","quantity":1200}}.
usage is optional for setup-only checks and required when the caller wants a
quota-aware decision for a specific unit count. Success returns 200 OK with
{"allowed":true,"reason":"billing_active"} or
{"allowed":false,"reason":"quota_exceeded","command":"bus billing setup"}.
Invalid requests return 400, missing or wrong internal authority returns
401 or 403, and unavailable billing storage returns 503.
GET /readyz
Reports process readiness.
Use this for load balancers and service supervisors. A ready process returns
200 OK with {"ok":true}. If the process is not accepting requests, the
supervisor or load balancer sees a connection failure or another non-2xx HTTP
status and should keep the instance out of rotation.
Catalog Rules
Catalog data should describe commercial plans in Bus terms. Common meters are
bus_llm_tokens and bus_container_runtime_seconds.
Plans may define several quota windows at the same time, such as minute,
hour, day, week, month, and total.
Do not store Stripe secret keys, webhook secrets, database passwords, or other deployment secrets in the catalog.
BUS_BILLING_DATABASE_URL
Enables durable PostgreSQL catalog storage.
Without this value, internal catalog endpoints return a deterministic storage-unavailable error instead of using volatile storage. Use a PostgreSQL URL with a role that can create and update the billing catalog tables and indexes in the selected database/schema:
postgres://bus_billing:password@postgres.example.internal:5432/bus_billing?sslmode=require
Use sslmode=disable only for trusted local development networks.
Events Backend
When configured with Events, the provider maps HTTP requests to billing events and waits for correlated responses.
Public status/setup requests use bus.billing.status.request,
bus.billing.checkout_session.request, and
bus.billing.portal_session.request.
Internal entitlement checks use bus.billing.entitlement.check.request.
Events
When configured with the Events backend, the provider triggers
bus.billing.status.request, bus.billing.checkout_session.request, and
bus.billing.portal_session.request, then returns the correlated responses.
Internal entitlement checks trigger bus.billing.entitlement.check.request and
return the correlated entitlement response.
Local Compose Stack
The BusDK superproject compose.yaml starts this provider as
bus-billing-api with --backend events and --events-url
http://bus-events:8081. Nginx publishes public billing endpoints at
/api/v1/billing/* and internal billing endpoints at
/api/internal/billing/* on the local API port. Billing state and entitlement
answers come from bus-integration-billing; this provider remains the
JWT-secured HTTP boundary for browser and API clients.
Deployment Checklist
Configure the provider with the same JWT audience and signing secret policy as
the rest of the Bus API deployment. Public billing endpoints accept only
end-user API JWTs. Internal endpoints accept only internal-audience JWTs.
Set BUS_BILLING_JWT_SECRET to the HS256 secret trusted for billing API
tokens, or use the deployment’s configured JWT secret source. Public requests
use audience ai.hg.fi/api; internal requests use ai.hg.fi/internal. The
scope contract is documented in
Bus API JWT audiences and scopes.
Start the provider with either the local deterministic backend or the Events backend. Events mode requires the Events URL and a provider token with billing event scopes:
bus operator token \
--api-url https://api.example.test \
--internal-key-file /run/secrets/bus-auth-internal-key \
--format token \
issue \
--subject billing-provider \
--audience ai.hg.fi/api \
--scope "billing:read billing:setup billing:entitlement:check billing:subscription:write billing:usage:export billing:provider" \
--ttl 1h > /run/secrets/bus-billing-provider.token
BUS_BILLING_JWT_SECRET="$(cat /run/secrets/bus-api-jwt-secret)" \
BUS_BILLING_DATABASE_URL="$(cat /run/secrets/bus-billing-database-url)" \
BUS_API_TOKEN="$(cat /run/secrets/bus-billing-provider.token)" \
bus-api-provider-billing \
--addr 0.0.0.0:8083 \
--backend events \
--events-url http://bus-events:8081
Run bus-integration-billing with durable storage so subscription state,
entitlements, idempotency keys, usage export state, and quota buckets survive
restarts. Run bus-integration-stripe or another payment-provider integration
when hosted setup, portal sessions, webhooks, and payment-meter events should
use a real provider.
Create a provider-neutral catalog file, publish it, and verify it through the
operator API:
the operator token must target the Billing API deployment and include
billing:catalog:read and billing:catalog:write.
bus operator token \
--api-url https://api.example.test \
--internal-key-file /run/secrets/bus-auth-internal-key \
--format token \
issue \
--subject billing-operator \
--audience ai.hg.fi/internal \
--scope "billing:catalog:read billing:catalog:write" \
--ttl 1h > /run/secrets/bus-billing-operator.token
mkdir -p ./local
bus operator billing catalog template > ./local/billing-catalog.json
bus operator billing \
--api-url https://api.example.test \
--token-file /run/secrets/bus-billing-operator.token \
catalog put --file ./local/billing-catalog.json
bus operator billing \
--api-url https://api.example.test \
--token-file /run/secrets/bus-billing-operator.token \
catalog get
Use bus operator stripe catalog sync --file ./local/billing-catalog.json when
Stripe products and prices should be synchronized from the same catalog.