Finance API
This API introduces a scoped finance RBAC role and the admin cost console, surfaced in
the UI as Settings › Costs › User Costs (route /finance). The
finance role manages billing settings and reads per-user cost without gaining the rest of
the administrative surface.
Base URL: https://<your-gateway-host>/admin/v1
The finance role
finance is a platform user role, alongside admin, tenant_admin, ki_manager, member,
viewer, demouser. A user's role is derived server-side from their sys:<role> role assignment
(the physical user.role column is being retired); it is never read from the client.
What finance can do (server-enforced — the client is never the authz boundary):
- Read per-user cost attribution for its own tenant —
GET /cogs/per-user(see below). - Read and write the gateway-owned billing settings —
GET/PUT /finance/billing-settings.
What finance cannot do — everything else on the admin surface is refused 403:
user/gateway/tenant management, Provider Costs (the /model-prices surface), the other /cogs/* and /stats* endpoints,
logs, audit log, health dashboard, and authoring (agents, scheduled tasks, workflows,
org-shares — finance is on the author deny-list, like viewer/demouser). finance retains
ordinary end-user capabilities (chat, profile).
Assignment. finance is assignable by admin and tenant_admin (via the standard user
create/update endpoints — see Users and tokens). A finance user assigns no
roles.
The role is always resolved from the authenticated user row, never from request input — so every gate below is authoritative regardless of what the client sends.
GET /cogs/per-user
Per-user cost attribution — cost and usage per user_id × period × model over the per-leg
billing ledger. The User Costs page's cost source (totals and the daily trend are derived from
these rows client-side).
Required role: admin, tenant_admin, or finance (widened this endpoint from
admin/tenant_admin; the added role is a superset, so existing access is unchanged). Every
non-admin caller — including finance — is pinned to its own tenant server-side,
fail-closed: a spoofed ?tenant_id is ignored, and a caller with no tenant is refused
403 { "error": "forbidden" }. A platform admin may target any tenant or omit ?tenant_id
for the global view.
The other cost breakdowns (/cogs/per-model, /cogs/per-service, /cogs/per-token,
/cogs/per-agent) remain admin/tenant_admin only — finance is refused 403 there.
Data-protection note. This response includes each user's email joined to their spend
(cost attribution is inherently per-identity). Widening it to finance therefore exposes
per-employee AI spend and identity to the finance role, within the caller's own tenant only.
This is the intended scope of "cost attribution per user"; confirm it against your DPA before
granting the role.
Query parameters (bucket, format, user_id, tenant_id, from/to, billable_only)
are unchanged and validated fail-closed — see the Stats API family conventions.
Billing-settings UI gate (billing_settings_enabled)
The invoice-recipient billing-settings section of the /finance console is hidden behind a
per-tenant feature flag, tenant.billing_settings_enabled (TINYINT, migration 0246),
defaulting 0 (OFF) — the invoice-recipient feature has no downstream consumer yet, so the
section stays hidden until the backend is real. Flip the tenant column to 1 to show it again.
The caller's effective flag is surfaced on the account payload as
tenant_billing_settings_enabled (0 | 1) by GET/PATCH /admin/auth/me (and the OTP-verify
login response), the same way tenant_workflows_enabled and the other tenant flags are. The SPA
renders the billing-settings section only when it is 1; absent/0 hides it (fail-closed).
This is a UI affordance gate only — the GET/PUT /finance/billing-settings endpoints below
are unchanged and remain reachable by finance/tenant_admin/admin regardless of the flag
(the client is never the authz boundary — invariant 11). The flag governs what the console shows,
not who may call the API.
GET /finance/billing-settings
Returns the gateway-owned billing settings for the caller's own tenant.
Required role: admin, tenant_admin, or finance. Operates on the caller's tenant
(me.tenant_id), fail-closed: a caller with no tenant is refused 403. (A platform admin
with no tenant of their own cannot use this endpoint; a manual tenant's settings are managed by
a finance/tenant_admin user inside that tenant.)
Response 200:
invoice_recipient— the email address invoices are addressed to, ornullwhen unset.plan— the tenant's current plan, display-only. Changing the plan is out of scope for this endpoint (it retunes entitlements/routing) and is tracked separately.
A transient backend fault returns 503 { "error": "temporarily unavailable; retry later" }.
PUT /finance/billing-settings
Updates invoice_recipient for the caller's own tenant. Required role: admin,
tenant_admin, or finance.
Request body:
- Accepted: a JSON string that is a valid email address, length ≤ 255. An empty
string (or an absent / explicit
nullfield) clears the setting (stores SQLNULL). - Rejected →
400: a non-string value (array, number, object), a value longer than 255 characters, or a string that is not a valid email address. Malformed input is rejected before any write.
Response 200 echoes the stored value: { "invoice_recipient": "billing@example.com" }
(or { "invoice_recipient": null } after a clear).
Errors: 400 invalid invoice_recipient; 403 caller is not finance/tenant_admin/
admin, or has no tenant; 500 on a storage failure.
Note:
invoice_recipientis not exported to CSV/XLSX today. If a future feature exports it, the value must be routed through the formula-injection-hardened CSV builder (a value starting with=,+,-, or@is escaped), the same as the COGS exports.