Skip to main content
POST
Create a token budget

Authorizations

Authorization
string
header
required

A Barndoor API key, sent as Authorization: Bearer <key>. Keys are prefixed bdai_ and are opaque — do not attempt to decode one. Interchangeable with an OAuth 2.0 access token.

Body

application/json
name
string
required
period
string
required
scope_type
string
required
token_limit
integer<int64>
required
action_on_exhaust
string
alert_thresholds
integer<int32>[]
change_note
string | null

Optional free-text note explaining why this budget was created (BCP-3998). Surfaced on the row and mirrored into the audit event.

cost_limit
number<double> | null
currency
string
member_of_group
string | null

BCP-3814: narrow this budget to the members of one IdP group. Only valid on a broad user-scoped budget (scope_type = "user" with neither scope_id nor scope_value), where the counter is already keyed per user — so each member of the group gets their own allowance from this single row. Handler-validated; V67 CHECK is the final safety net.

scope_id
string<uuid> | null
scope_value
string | null
target_mcp_server_id
string<uuid> | null

BCP-4053 Target dimension: when set, the budget only fires on requests routed to this MCP server. Requires MCP-capable traffic and is mutually exclusive with the three LLM target dimensions (validated at the handler; the V73 CHECKs are the final safety net).

target_model_alias
string | null

BCP-2813 Target dimension: matches the user-facing alias from request.model. Mutually exclusive with the other two.

target_provider_id
string<uuid> | null

BCP-2813 Target dimension: when set, the budget only fires on routes whose provider id matches. May be combined with target_upstream_model; mutually exclusive with target_model_alias (validated at the handler; the DB CHECK is the final safety net).

target_upstream_model
string | null

BCP-2813 Target dimension refining target_provider_id: matches the route's resolved upstream model name. Requires target_provider_id — a bare upstream name would string every provider serving that model into one cap (handler-enforced).

traffic_type
string

Response

The created record

An admin-API response for an entity whose core type lives in ai_governance and so cannot carry portal-only columns.

TokenBudget and RateLimitPolicy are read by the enforcement path on every request; hanging five display-only fields off them would put portal concerns in the hot path's type and force every governance consumer to carry them. Wrapping instead keeps the enforcement reads (get_enabled_by_org and friends) returning the bare governance type, while the admin reads return this.

Both halves are #[serde(flatten)], so the wire shape stays the flat object the portal already consumes — same choice AdminModelPricing makes.

action_on_exhaust
enum<string>
required
Available options:
block,
throttle,
warn
alert_thresholds
integer<int32>[]
required
enabled
boolean
required
id
string<uuid>
required
name
string
required
org_id
string<uuid>
required
period
enum<string>
required

How often a budget's counter resets.

Every period boundary is UTC, with no per-org configuration: key_suffix formats Utc::now(), so Daily rolls at 00:00 UTC, Weekly at Monday 00:00 UTC (ISO-8601 week), and Monthly on the 1st at 00:00 UTC. An admin in a western timezone therefore sees their "daily" cap reset in the afternoon — [Self::period_end] exists so the UI can say so rather than leave them to infer it (BCP-4204).

Available options:
daily,
weekly,
monthly
scope_type
enum<string>
required

Scope is the identity axis of a budget or rate-limit policy — "who is this policy for?". The what (LLM provider, upstream model name, model alias) lives on dedicated target_* columns on the row (BCP-2813), not as a scope value. LlmProvider and Model remain in the enum because rate_limit_policies and model_access_policies still accept them in their scope_type CHECK constraints (V12 / V18 / V22 widenings — a separate scope of work to clean up). For token budgets the CHECK was tightened back to identity-only by V41__budget_scope_target.sql, so any LlmProvider/Model budget row that survived would simply fail to match in BudgetEnforcer::matched_keys.

Available options:
org,
team,
user,
project,
api_key,
mcp_server,
llm_provider,
model,
agent,
role,
group
token_limit
integer<int64>
required
traffic_type
enum<string>
required
Available options:
all,
llm,
mcp
cost_limit
number<double> | null

Optional spending cap in the configured currency. When set, the budget is enforced on estimated cost in addition to tokens.

created_at
string<date-time> | null

When the budget was created. Populated on the admin API path (where it is surfaced in the dashboard); left None on the hot enforcement path and in fixtures, which don't need it. #[serde(default)] keeps older payloads without the field deserializing cleanly.

currency
string

ISO 4217 currency code (e.g. "USD"). Defaults to "USD".

member_of_group
string | null

BCP-3814 membership filter: when set, this budget only applies to requesters whose IdP group membership includes this group name.

Only meaningful on a broad user-scoped budget (scope_type == User with scope_id and scope_value both None), where the counter qualifier already resolves to the requesting user's id — so one budget row gives every member of the group their own independent allowance, and a user joining the group is enforced on their next request with no admin action. DB-enforced by token_budgets_member_of_group_requires_broad_user (V65, tightened to exclude scope_value by V67).

None ⇒ the budget applies org-wide (the pre-BCP-3814 behaviour). Contrast with a ScopeType::Group budget, which keys its counter by the group name and so pools one allowance across all members.

scope_id
string<uuid> | null
scope_value
string | null
target_mcp_server_id
string<uuid> | null

BCP-4053 Target dimension: when set, the budget only fires on requests routed to this MCP server. The MCP-traffic counterpart of target_provider_id, and mutually exclusive with all three LLM target columns (DB-enforced by token_budgets_mcp_target_exclusive_check) — no single request carries both an MCP server and an LLM route, so a row naming both could never match.

Distinct from a ScopeType::McpServer budget, which puts the server on the identity axis and so cannot be combined with a user or group. As a target it composes freely with any scope, which is what makes "this user's cap on this server" expressible.

target_model_alias
string | null

BCP-2813 Target dimension: when set, the budget only fires when the requested model alias (request.model) equals this value. Mutually exclusive with the other two target_* fields.

target_provider_id
string<uuid> | null

BCP-2813 Target dimension: when set, the budget only fires on routes whose provider id equals this value. May be combined with target_upstream_model to cap a single model on this provider; mutually exclusive with target_model_alias (DB-enforced by token_budgets_alias_target_exclusive_check).

target_upstream_model
string | null

BCP-2813 Target dimension refining target_provider_id: when set, the budget only fires on routes whose resolved upstream model name equals this value (string match). Always set alongside a provider (V54 CHECK-enforced) — a bare upstream name would string every provider serving that model into one cap.

created_by_email
string | null
created_by_name
string | null
created_by_user_id
string<uuid> | null
last_change_note
string | null

Optional free text explaining why the last change was made ("Raised for the Q3 batch job", "Vendor deprecated the old endpoint").

Admin-authored and rendered in the portal, so it is untrusted input: length-capped here, escaped on render.

last_modified_at
string<date-time> | null

When the change named by last_modified_by_* happened.

Its own column rather than the row's updated_at, because updated_at is bumped by writes nobody is attributed for -- pairing the two would date a named admin's edit to a background job's clock. See the V75 migration for the full reasoning.

last_modified_by_email
string | null
last_modified_by_name
string | null
last_modified_by_user_id
string<uuid> | null