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 path | Who | Does |
|---|---|---|
GET /v1/budget-pools | Any signed-in user | Lists the pools you can see. Admins get all of them, everyone else gets their own. |
POST /v1/budget-pools | Admin | Creates a pool. |
GET /v1/budget-pools/{id} | Admin, lead, member | One pool with its members and, for admins and leads, its warnings. 404 for anyone else. |
PATCH /v1/budget-pools/{id} | Admin | Edits name, limit, lead, dates, burst, forecast and alert settings. |
PUT /v1/budget-pools/{id}/allocation | Admin, lead | Sets equal or manual shares, and optionally the guaranteed percentage. |
POST /v1/budget-pools/{id}/members | Admin | Adds 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} | Admin | Removes a key from the pool. |
GET /v1/budget-pools/{id}/alerts | Admin, lead, member | The pool's alert history. Members see the pool's alerts and their own share's. |
GET /v1/budget-pools/{id}/forecast | Admin, lead, member | Projected run-out date for the pool and each member. 409 if forecast is off. |
GET /v1/budget-pools/notifications | Any signed-in user | Your in-app pool alerts. unread_only and limit are optional. |
POST /v1/budget-pools/notifications/read-all | Any signed-in user | Marks your pool alerts read. |
POST /v1/keys/{id}/reveal | The key's owner, signed in to the console | Reveals a key that was issued for them by adding them to a pool. The secret is returned once. |
GET /v1/notifications/mine | Any signed-in user | Notifications 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} | Admin | Archives the pool and releases its members. Works on any plan. |
DELETE /v1/budget-pools/{id}/permanent | Admin | Deletes 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]
}'import httpx
r = httpx.post(
"https://api.forgebench.ai/v1/budget-pools",
headers={"Authorization": f"Bearer {admin_token}"},
json={
"name": "Platform team",
"monthly_limit_usd": "1000",
"identity_ids": [priya, sam],
"api_key_ids": [ci_pipeline_key],
"lead_identity_id": priya,
},
)
r.raise_for_status()
pool = r.json()Request fields
| Field | Type | Notes |
|---|---|---|
name | string | Unique in the workspace. 409 pool_name_taken otherwise. |
kind | developer or agent | Default 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_usd | string | Not above the workspace's monthly budget. |
api_key_ids | uuid list | Keys to put in the pool. A key can be in only one pool. At most 500. |
identity_ids | uuid list | People 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_id | uuid or null | Must 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_share | bool | Default true. With false, send allocations. |
allocations | map of id to amount | Only 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_pct | 1 to 100 | Default 100. Below 100 turns on burst: the rest of the limit is the shared zone. |
member_max_usd | string or null | With burst, the most one key can spend in total. Must be at least their share and at most the limit. |
starts_at, ends_at | ISO timestamps | Optional time-box. ends_at must be in the future and after starts_at. |
period | monthly or window | window is one limit for the whole span and needs both dates. |
forecast_enabled | bool | Default false. |
alert_thresholds | int list | Whole numbers 1 to 100. Default 50, 75, 90, 95, 100. |
alert_in_app_same_as_email | bool | Default 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_ids | bool, if_no_lead / always / never, uuid list | Who 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:
| Field | Meaning |
|---|---|
kind | developer or agent. |
status | scheduled, active, expired or archived. |
spent_usd, remaining_usd | Spend this period and what is left of the limit. |
shared_zone_usd | The part of the limit that is not split into shares. |
my_share | The 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_allocations | What the caller may do. Use these to decide which buttons to show. |
members | Each 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. |
warnings | List 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_exceededandpool_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
| Status | Code | Cause |
|---|---|---|
| 402 | plan_required | The workspace is on a plan without budget pools. |
| 403 | admin_required | The call needs an admin. |
| 403 | not_pool_lead | Only the pool's lead or an admin can change shares. |
| 404 | pool_not_found | No such pool, or it is not yours to see. |
| 409 | pool_name_taken | Another pool has this name. |
| 404 | key_not_found, member_not_found | No such active key in this workspace (a revoked key cannot join), or that key is not in this pool. |
| 409 | already_in_pool | A key can be in only one pool. The body names the pool. Also returned when a person already has a key in this pool. |
| 409 | key_not_revealed, already_revealed | Rotating a key its owner has not revealed yet (only the owner may see its secret), and revealing a key that was already revealed. |
| 409 | pool_not_active, pool_archived | The pool is over or archived and cannot be edited. |
| 409 | forecast_disabled | Forecast is not turned on for this pool. |
| 422 | pool_limit_exceeds_workspace_budget | The limit is above the workspace's monthly budget. The body has workspace_limit_usd. |
| 422 | allocations_exceed_limit | Shares add up to more than the guaranteed part of the limit. |
| 422 | pool_limit_below_allocations | The new limit or guaranteed percentage is below what is already divided. |
| 422 | allocation_for_non_member, allocation_negative, allocations_need_manual_mode, share_needs_manual_mode | An invalid share request. |
| 422 | lead_not_a_member | The lead must be an admin or an owner, or own one of the pool's keys. |
| 422 | key_kind_mismatch | The 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. |
| 422 | agent_pool_takes_agent_keys | A person was added to an agent pool. |
| 422 | choose_a_key, developer_cannot_hold_key | Adding a person: they have several free keys (the body lists them), or their role cannot hold a key. |
| 422 | window_needs_both_dates, window_end_before_start, window_end_in_past | Invalid dates for a time-boxed pool. |
| 422 | member_max_needs_burst, member_max_below_share, member_max_above_limit | Invalid member_max_usd. |
| 422 | insufficient_unallocated_headroom | A 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.

