Administration Budgets Pool API and errors

Pool API and errors

Everything the Pools tab does is available over HTTP. Management calls need an admin credential unless the table says a lead may make them. All amounts are USD strings with up to six decimals.

Endpoints

Method and pathWhoDoes
GET /v1/budget-poolsAny signed-in userLists the pools you can see. Admins get all of them, everyone else gets their own.
POST /v1/budget-poolsAdminCreates a pool.
GET /v1/budget-pools/{id}Admin, lead, memberOne pool with its members and, for admins and leads, its warnings. 404 for anyone else.
PATCH /v1/budget-pools/{id}AdminEdits name, limit, lead, dates, burst, forecast and alert settings.
PUT /v1/budget-pools/{id}/allocationAdmin, leadSets equal or manual shares, and optionally the guaranteed percentage.
POST /v1/budget-pools/{id}/membersAdminAdds a member. Send api_key_id for a key (a service key, an agent's key or a specific developer key) or identity_id for a person, who brings their one free key or is issued one. Exactly one of the two.
DELETE /v1/budget-pools/{id}/members/{api_key_id}AdminRemoves a key from the pool.
GET /v1/budget-pools/{id}/alertsAdmin, lead, memberThe pool's alert history. Members see the pool's alerts and their own share's.
GET /v1/budget-pools/{id}/forecastAdmin, lead, memberProjected run-out date for the pool and each member. 409 if forecast is off.
GET /v1/budget-pools/notificationsAny signed-in userYour in-app pool alerts. unread_only and limit are optional.
POST /v1/budget-pools/notifications/read-allAny signed-in userMarks your pool alerts read.
POST /v1/keys/{id}/revealThe key's owner, signed in to the consoleReveals a key that was issued for them by adding them to a pool. The secret is returned once.
GET /v1/notifications/mineAny signed-in userNotifications addressed to you, such as key_issued. Also POST /v1/notifications/mine/{id}/read and POST /v1/notifications/mine/read-all.
DELETE /v1/budget-pools/{id}AdminArchives the pool and releases its members. Works on any plan.
DELETE /v1/budget-pools/{id}/permanentAdminDeletes the pool. Releases the members of a live pool. Works on any plan.

On a Free plan every call except the two deletes returns 402.

Create a pool

curl -X POST https://api.forgebench.ai/v1/budget-pools \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
  "name": "Platform team",
  "monthly_limit_usd": "1000",
  "auto_share": false,
  "guaranteed_pct": 75,
  "identity_ids": ["<priya-id>", "<sam-id>"],
  "api_key_ids": ["<ci-pipeline-key-id>"],
  "lead_identity_id": "<priya-id>",
  "allocations": {"<priya-id>": "300", "<sam-id>": "250", "<ci-pipeline-key-id>": "200"},
  "alert_thresholds": [50, 75, 90, 100]
}'

Request fields

FieldTypeNotes
namestringUnique in the workspace. 409 pool_name_taken otherwise.
kinddeveloper or agentDefault developer. A developer pool takes keys that are not an agent's credential (people's and service keys) and people by identity_ids. An agent pool takes agent credentials only and no people. Fixed at creation.
monthly_limit_usdstringNot above the workspace's monthly budget.
api_key_idsuuid listKeys to put in the pool. A key can be in only one pool. At most 500.
identity_idsuuid listPeople to add. Each brings their one free key, or is issued one that only they can reveal (POST /v1/keys/{id}/reveal). 422 choose_a_key if they have several free keys, and 422 developer_cannot_hold_key if their role cannot hold a key.
lead_identity_iduuid or nullMust be an active admin or owner, or the owner of one of the pool's keys (422 lead_not_a_member). An existing lead is checked only when you change it, and is cleared if removing a key leaves them neither.
auto_shareboolDefault true. With false, send allocations.
allocationsmap of id to amountOnly with auto_share: false. Keyed by key id, or by identity id for a person in identity_ids. Keys not listed get a share of zero. The total cannot exceed the guaranteed part of the limit.
guaranteed_pct1 to 100Default 100. Below 100 turns on burst: the rest of the limit is the shared zone.
member_max_usdstring or nullWith burst, the most one key can spend in total. Must be at least their share and at most the limit.
starts_at, ends_atISO timestampsOptional time-box. ends_at must be in the future and after starts_at.
periodmonthly or windowwindow is one limit for the whole span and needs both dates.
forecast_enabledboolDefault false.
alert_thresholdsint listWhole numbers 1 to 100. Default 50, 75, 90, 95, 100.
alert_in_app_same_as_emailboolDefault true on create. The in-app alert goes to the active users whose email is on an enabled email alert channel that covers the pool. The next three fields are used only when this is false.
alert_notify_lead, alert_notify_admins, alert_extra_identity_idsbool, if_no_lead / always / never, uuid listWho gets the in-app alert. Email goes through alert channels scoped to the pool.

The response is the pool. The fields you will use most:

FieldMeaning
kinddeveloper or agent.
statusscheduled, active, expired or archived.
spent_usd, remaining_usdSpend this period and what is left of the limit.
shared_zone_usdThe part of the limit that is not split into shares.
my_shareThe caller's total across the keys they own in the pool: api_key_ids, share, cap and spend. Null if they own none.
can_manage, can_edit_allocationsWhat the caller may do. Use these to decide which buttons to show.
membersEach key with api_key_id, key_name, its owner (owner_identity_id, owner_name; empty for a service key), pending_reveal (true until the owner reveals a key issued to them), share_usd, max_usd, spent_usd and is_lead. A pending key's prefix is not shown.
warningsList of code and message. See Shares and burst.

Set shares

curl -X PUT https://api.forgebench.ai/v1/budget-pools/$POOL_ID/allocation \
-H "Authorization: Bearer $LEAD_OR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
  "auto_share": false,
  "guaranteed_pct": 80,
  "allocations": {"<priya-key-id>": "300", "<sam-key-id>": "300", "<ci-key-id>": "200"}
}'

guaranteed_pct is optional. Send it to change the burst split in the same save. The allocations are checked against the new guaranteed part, and nothing changes if they do not fit.

What a refused call looks like

These come back from any governed call, for example POST /v1/chat/completions.

// 402 — the pool's monthly limit would be exceeded
{
"detail": {
  "code": "pool_budget_exceeded",
  "message": "Budget pool “Platform team” has reached its $1000.00 limit ($996.00 spent this period). Ask Priya Shah to raise the limit.",
  "limit_kind": "pool_total",
  "pool_id": "6f3c…",
  "pool_name": "Platform team",
  "pool_owner": "Priya Shah",
  "key_id": "b21e…",
  "key_name": "laptop",
  "key_owner": "Ada Lovelace",
  "limit_usd": "1000.000000",
  "spent_usd": "996.000000",
  "attempted_cost_usd": "6.000000"
}
}

// 402 — this key's share is used up; the pool may still have room
{
"detail": {
  "code": "pool_member_share_exceeded",
  "message": "Key “laptop” (Ada Lovelace) has used its $300.00 share of budget pool “Platform team” ($297.00 spent this period). Ask Priya Shah to raise the share.",
  "limit_kind": "pool_member_share",
  "pool_id": "6f3c…",
  "pool_name": "Platform team",
  "pool_owner": "Priya Shah",
  "key_id": "b21e…",
  "key_name": "laptop",
  "key_owner": "Ada Lovelace",
  "limit_usd": "300.000000",
  "spent_usd": "297.000000",
  "attempted_cost_usd": "6.000000"
}
}

In pool_member_share_exceeded, limit_usd is the key's cap (its share, plus the shared zone if burst allows it), not the pool's limit. limit_kind says which limit fired (pool_total or pool_member_share; a key's own caps report key_daily or key_monthly) and pool_owner and key_owner say whose it is. A service key has no key_owner, and a pool with no lead has no pool_owner.

Handle them by detail.code:

  • pool_budget_exceeded and pool_member_share_exceeded: the budget is the problem. Queue the work, use a cheaper model, or tell the user. Do not retry in a loop.

Management errors

StatusCodeCause
402plan_requiredThe workspace is on a plan without budget pools.
403admin_requiredThe call needs an admin.
403not_pool_leadOnly the pool's lead or an admin can change shares.
404pool_not_foundNo such pool, or it is not yours to see.
409pool_name_takenAnother pool has this name.
404key_not_found, member_not_foundNo such active key in this workspace (a revoked key cannot join), or that key is not in this pool.
409already_in_poolA key can be in only one pool. The body names the pool. Also returned when a person already has a key in this pool.
409key_not_revealed, already_revealedRotating a key its owner has not revealed yet (only the owner may see its secret), and revealing a key that was already revealed.
409pool_not_active, pool_archivedThe pool is over or archived and cannot be edited.
409forecast_disabledForecast is not turned on for this pool.
422pool_limit_exceeds_workspace_budgetThe limit is above the workspace's monthly budget. The body has workspace_limit_usd.
422allocations_exceed_limitShares add up to more than the guaranteed part of the limit.
422pool_limit_below_allocationsThe new limit or guaranteed percentage is below what is already divided.
422allocation_for_non_member, allocation_negative, allocations_need_manual_mode, share_needs_manual_modeAn invalid share request.
422lead_not_a_memberThe lead must be an admin or an owner, or own one of the pool's keys.
422key_kind_mismatchThe key does not fit the pool: an agent's credential in a developer pool, or a person's or service key in an agent pool.
422agent_pool_takes_agent_keysA person was added to an agent pool.
422choose_a_key, developer_cannot_hold_keyAdding a person: they have several free keys (the body lists them), or their role cannot hold a key.
422window_needs_both_dates, window_end_before_start, window_end_in_pastInvalid dates for a time-boxed pool.
422member_max_needs_burst, member_max_below_share, member_max_above_limitInvalid member_max_usd.
422insufficient_unallocated_headroomA share increase needs more undivided budget than the pool has.

Amounts must be finite, non-negative and at most 999999.999999. One request adds at most 500 keys or people.