Skip to content

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": "billing@example.com", "plan": "enterprise" }
  • invoice_recipient — the email address invoices are addressed to, or null when 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:

{ "invoice_recipient": "billing@example.com" }
  • Accepted: a JSON string that is a valid email address, length ≤ 255. An empty string (or an absent / explicit null field) clears the setting (stores SQL NULL).
  • 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_recipient is 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.


See also