> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sunra.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Shared budget pools

> Authorize multiple budget keys and external operations against one temporary USD budget.

A budget pool lets multiple budget keys share one temporary authorization. Each key remains an independent billing ticket with its own calls and final receipt. Creating a pool or issuing a key does not transfer money or create an organization charge. Normal organization billing and model access checks still apply to every LLM request.

This public feature is initially enabled for selected organizations. Contact Sunra to enable it. A disabled pool request returns `503 budget_pools_disabled`; do not retry it as an unpooled request.

## Amounts and responsibility

Pool amounts are decimal USD strings with at most eight fractional digits on input and exactly eight on output. JSON numbers, negative inputs, exponent notation, null, and extra precision are invalid. The maximum admission amount is `90071992.54740991` USD. Authorization may be zero; credits, debits, and an optional child cap must be positive.

A pool exposes four amounts:

| Field                | Meaning                                                            |
| -------------------- | ------------------------------------------------------------------ |
| `authorized_usd` (A) | Cumulative authorization granted to this pool.                     |
| `committed_usd` (C)  | Persisted paid LLM spend plus successful external debits.          |
| `in_flight_usd` (F)  | Reservations for calls whose financial outcome is not yet settled. |
| `available_usd` (R)  | A minus C minus F.                                                 |

Authorization is **not a mirror of your remaining wallet balance**. A credit increases A; it never reduces C. For example, authorize `10`, debit `3`, then credit `1`: A is `11`, C is `3`, and R is `8`. Setting A to the wallet's remaining `8` would deduct the same consumption twice.

Sunra maintains C and F. You cannot overwrite them. Setting A below C plus F pauses the pool immediately and may make R negative. Previously admitted work still settles at its actual recorded amount. Adding authorization does not resume a paused pool automatically.

An LLM call reserves its estimated maximum sell cost before dispatch. One atomic decision checks both the shared pool and the child's optional cap. External debits compete with those same reservations and commit immediately. A decision that leaves exactly zero available is allowed. Rejected debits do not partially consume authorization and never produce LLM rows or child receipts.

## Authentication and management

Manage pools and keys on `https://api.sunra.ai/v1` with an active ordinary API key. Use the issued child secret on `https://api-llm.sunra.ai/v1` for LLM calls. Budget children and user sessions cannot manage pools.

`management` is immutable and defaults to `org`:

* `org`: active ordinary keys in the same organization can manage the pool.
* `parent_only`: the issuing parent manages the pool while it remains active. If that parent is definitively inactive, revoked, suspended, or missing, another active ordinary key in the same organization can recover, read, pause, and close it. A failed parent lookup does not grant recovery access.

Only the original active parent can mint new pooled children. Children inherit the pool's parent and management mode. A replacement key should finish the old pool and create a new generation. Foreign-organization pool IDs return `404 budget_pool_not_found`. Management protection is not a separate confidentiality boundary within an organization; existing organization-wide completion access is unchanged.

After the original parent becomes inactive, a recovery key cannot debit or resume that pool. It can still confirm historical results, add a repair credit, pause, and close. Confirmed operation replays preserve their original results.

## Create and recover a pool

```http theme={null}
POST /v1/budget-pools
Authorization: Bearer <ordinary-api-key>
Content-Type: application/json

{
  "ref": "account-opaque-reference",
  "authorized_usd": "10.00000000",
  "expected_generation": "0",
  "management": "parent_only"
}
```

The first creation returns `201` and a `budget_pool` resource. The creation identity is organization, `ref`, and `expected_generation`. Repeating the same normalized parameters returns `200` with the same pool ID and its current snapshot. Different parameters return `409 budget_pool_conflict`. `ref` must contain 1–256 characters.

Use `expected_generation: "0"` for the first pool. After generation `"1"` is closed, use `expected_generation: "1"` to create generation `"2"`. A new generation has a new pool ID. An old create retry always finds its original pool; it cannot open a new generation. Only one current generation can exist per organization and ref.

```http theme={null}
GET /v1/budget-pools/{pool_id}
GET /v1/budget-pools?ref=account-opaque-reference&state=active&limit=100
```

The list accepts optional exact `ref` and `state` (`active`, `paused`, or `closed`) filters, `limit` from 1 through 100 (default 100), and an opaque `after` cursor. It returns `{data, has_more, next_cursor}` in newest-creation order, which gives descending generations for a ref. Authorization is applied before pagination. Keep filters and the calling credential unchanged when following a cursor. Lists are not financial snapshots across pages.

A pool resource includes `id`, `organization`, `ref`, `generation`, `version`, `state`, the four amounts, `management`, `parent_api_key_id`, `created_at`, `last_admitted_at`, `pause_reason`, `close_requested_at`, `closed_at`, `closed_by_api_key_id`, and `open_children`. Unset lifecycle timestamps and attribution are null. Generation and version are decimal **strings**. Versions increase on actual state changes and can jump after recovery; they are not wallet event watermarks.

## Issue a turn key

```http theme={null}
POST /v1/budget-keys
Authorization: Bearer <ordinary-api-key>
Content-Type: application/json

{
  "pool_id": "<pool-id>",
  "pool_generation": "1",
  "ref": "turn-opaque-reference",
  "expires_at": "2026-09-20T12:00:00Z",
  "models": ["openai/gpt-5.5"]
}
```

Choose a future expiry no later than seven days after the pool was created. `ref` is required and identifies one logical ticket within the pool. Existing `models`, `metadata`, and call-tag restrictions still apply. You may add a positive `cap_usd` as a second, child-specific bound; omitting it leaves the key subject to the shared pool alone. The child resource omits `cap_usd` when none was set and includes `pool_id` and `pool_generation`.

The first successful mint returns `201` with one secret. The same pool/ref and parameters return `200` with the same child and **no secret**. Different parameters return `409 budget_key_mint_conflict`. Only the secret hash is stored. If the initial response was lost before you saved the secret, recover the child ID, close that ticket, and finish its accounting. Do not reuse its ref to create another ticket or expect to recover the secret.

```http theme={null}
GET /v1/budget-keys?pool_id={pool_id}&ref={turn_ref}
GET /v1/budget-keys/{child_id}
GET /v1/budget-keys/{child_id}/calls
POST /v1/budget-keys/{child_id}/close
```

The pool selector may be used without ref. Existing child list pagination and permissions apply. Closing A stops A's new calls while B can continue using the pool. A's accepted calls remain responsible for their costs. A pooled child that expires stops accepting new calls but does not automatically close its financial ticket. Explicitly close it and wait for `status: "closed"` before treating its receipt as final.

Pooled child close returns `200` with `finalizing` or `closed`; inspect the resource status. Receipts retain version 1, call identifiers, sealed paid amounts, and full pagination. Fetch all call pages and verify their paid sum against the child's final spend. External operations are excluded from these receipts. Recording that receipt in your wallet must not debit the pool again.

Each ticket has one secret. This release does not support extending expiry or replacing the secret on an existing ticket.

## Change authorization or pause admission

```http theme={null}
PATCH /v1/budget-pools/{pool_id}
If-Match: "<version>"
Content-Type: application/json

{
  "authorized_usd": "8.00000000",
  "generation": "1",
  "mutation_id": "authorization-event-42"
}
```

PATCH sets cumulative A absolutely. It requires the version from a prior snapshot in `If-Match` so a stale wallet update cannot overwrite a concurrent credit. A stale version returns `409 budget_pool_version_conflict`.

```http theme={null}
POST /v1/budget-pools/{pool_id}/pause

{"generation":"1","mutation_id":"pause-42"}
```

Pause immediately stops new LLM admissions, debits, and mints. It does not require `If-Match`. Reads, settlement, and credits remain available.

```http theme={null}
POST /v1/budget-pools/{pool_id}/resume
If-Match: "<version>"

{"generation":"1","mutation_id":"resume-42"}
```

Resume requires known accounting, sufficient authorization for existing liabilities, no close request, and an age below seven days. It does not restore spent authorization. A successful resume starts a fresh idle window. PATCH and lifecycle mutations use 1–128-character mutation IDs: the same ID and parameters replay the stored decision; changed parameters return `409 budget_mutation_conflict`. Replays do not increment the version.

A pool pauses after 24 hours without a successful LLM admission or external debit, or seven days after creation. Reads, failed admissions, credits, and PATCH do not extend its activity window. Expiry never automatically closes the pool. An over-age pool must be finished and replaced with a new generation.

## Debit, credit, and recover an operation

```http theme={null}
POST /v1/budget-pools/{pool_id}/operations
Content-Type: application/json

{
  "operation_id": "generation-order-42",
  "type": "debit",
  "amount_usd": "0.75000000",
  "generation": "1",
  "reason": "External generation authorization"
}
```

An applied first operation returns `201`; an applied replay returns `200`. Persist your operation ID before making the request. Do not start external work until its debit is confirmed applied. The external work's later success does not debit this pool again.

Use `type: "credit"` to grant more authorization after a confirmed top-up or refund. A credit may include `related_operation_id` pointing to an applied debit in the same pool. This link is for audit; Sunra does not implement your order or refund state machine. `reason` is optional, at most 256 characters, and has no authorization effect. Operation IDs are opaque strings of 1–128 characters.

```http theme={null}
GET /v1/budget-pools/{pool_id}/operations/{operation_id}
GET /v1/budget-pools/{pool_id}/operations?limit=100&after={cursor}
```

Operation resources contain the submitted parameters, `status` (`pending`, `applied`, or `rejected`), timestamps, `result_version`, and the decision's pool snapshot. Applied and rejected results are immutable. A replay returns the original decision snapshot, even if later operations changed the pool. Amounts compare after normalization; type, generation, reason, and related ID also participate in conflict detection.

A store timeout with an unresolved decision can return `202 pending`. Query or retry **the same ID**. A `404` only means no durable intent was found; it does not authorize choosing a new ID. A rejected debit remains rejected after later credit; a genuinely new attempt needs a new persisted operation ID. A changed request under an existing ID returns `409 budget_operation_conflict`.

Operation lists use ascending creation order, an opaque pool-bound cursor, and the same 1–100 page size. Poll pending IDs separately because a paginated scan is not a stream of status changes.

## Close a pool

```http theme={null}
POST /v1/budget-pools/{pool_id}/close
Content-Type: application/json

{"generation":"1","mutation_id":"close-42","force":false}
```

Close without force rejects with `409 budget_pool_children_open` if any child or mint membership is still open. It has no closing side effect in that case. `force: true` first stops pool admission, then closes children through their existing finalization path, including children whose mint response is still being recovered.

A completed close returns `200 closed`. If accepted responsibilities or membership outcomes remain unresolved, it returns `202 paused` with `close_requested_at` set. Retry the same close operation. A close request is irreversible: new mints, resume, PATCH, debits, and credits are blocked, but accepted work can settle. Closure does not refund R because no funds were transferred into the pool.

Closed pools never reopen. Their snapshots, operations, child records, and receipts remain available for audit. Old IDs and generations cannot mutate a new generation. Confirmed historical operations still replay after closure.

## Poolable models and ceiling translation

Pooled children use an explicit model allowlist: only models whose billed output is verified to stay within the requested output ceiling are admitted. Unlisted models return `400 model_not_poolable`, including the five reasoning Grok models `grok-4.20`, `grok-4.3`, `grok-4.5`, `grok-4.6`, and `grok-build-0.1`. Ordinary keys and unpooled children keep their existing behavior.

The following eight models require Chat Completions translation:

* Ark: `doubao-seed-2.1-pro`, `doubao-seed-2.1-turbo`, `doubao-seed-evolving`, `glm-5.2`.
* DashScope: `qwen3.7-flash`, `qwen3.7-plus`, `qwen3.7-max`, `qwen3.8-max`.

For these models, Sunra sends the admitted ceiling as `max_completion_tokens` and removes `max_tokens` from the upstream copy. This ceiling includes reasoning output. The client body, reasoning intent, and reservation formula are unchanged. Pooled Responses and Messages requests on these eight models return `400 invalid_input` because the translation is not verified for those formats.

The 75 native-ceiling model IDs are (83 poolable chat models including the eight translated IDs):

* `claude-fable-5`
* `claude-fable-5-1`
* `claude-opus-4-7`
* `claude-opus-4-8`
* `claude-opus-5`
* `claude-sonnet-5`
* `gpt-5.4-pro`
* `claude-haiku-4-5-aws`
* `claude-opus-4-6-aws`
* `claude-opus-4-7-aws`
* `claude-opus-4-8-aws`
* `claude-sonnet-4-6-aws`
* `claude-haiku-4-5-reverse`
* `claude-opus-4-6-reverse`
* `claude-sonnet-4-6-reverse`
* `claude-haiku-4-5-relay`
* `claude-opus-4-6-relay`
* `claude-sonnet-4-6-relay`
* `qwen3.5-plus`
* `claude-haiku-4-5`
* `claude-haiku-4-5-openrouter`
* `claude-opus-4-6`
* `claude-opus-4-6-openrouter`
* `claude-sonnet-4-6`
* `claude-sonnet-4-6-openrouter`
* `deepseek-flash`
* `deepseek-v4-flash`
* `deepseek-v4-flash-vision-exp`
* `deepseek-v4-pro`
* `gemini-2.5-flash`
* `gemini-2.5-flash-gcp`
* `gemini-2.5-flash-openrouter`
* `gemini-2.5-pro`
* `gemini-2.5-pro-gcp`
* `gemini-2.5-pro-openrouter`
* `gemini-3.1-flash-lite-preview`
* `gemini-3.1-pro-preview`
* `gemini-3.1-pro-preview-gcp`
* `gemini-3.1-pro-preview-openrouter`
* `gemini-3.5-flash`
* `gemini-3.5-flash-gcp`
* `gemini-3.5-flash-lite`
* `gemini-3.6-flash`
* `gemini-3.7-flash`
* `gemini-3.8-flash`
* `glm-5`
* `glm-5-turbo`
* `glm-5.3`
* `glm-5.3-flash`
* `gpt-4o-mini`
* `gpt-5-mini`
* `gpt-5-nano`
* `gpt-5.3-codex`
* `gpt-5.4`
* `gpt-5.5`
* `gpt-5.6-luna`
* `gpt-5.6-sol`
* `gpt-5.6-terra`
* `gpt-6-astra`
* `gpt-oss-120b`
* `grok-4.20-non-reasoning`
* `kimi-k2.6`
* `kimi-k2.7-code`
* `kimi-k2.7-code-highspeed`
* `kimi-k3`
* `longcat-2.0`
* `minimax-m2-her`
* `minimax-m2.1`
* `minimax-m2.5`
* `muse-spark-1.1`
* `muse-spark-1.2`
* `o4-mini`
* `seed-2.0-lite`
* `seed-2.0-mini`
* `o3`

Claude `-reverse` and `-relay` aliases are admitted because they serve the same Claude models on Anthropic-format upstreams. Legacy `-aws` aliases can degrade to the unpinned default routes. Only upstream routes qualified under the same ceiling rule may execute; a request that resolves to an unqualified route fails closed with `400 model_not_poolable`.

Text `/v1/embeddings` requests, including `gemini-embedding-2`, retain prompt-only reservation and need no completion allowlist entry. Native/media embeddings remain forbidden for child keys. `minimax-m2.7` is delisted.

These are exact released model names after public-model resolution, not family or prefix permissions. New models and aliases require a billed-output ceiling acceptance check before being added. `grok-4.20-non-reasoning` is a separate measured native entry; it does not authorize any reasoning Grok sibling.

For Chat Completions, provide a positive integer `max_tokens` or `max_completion_tokens`. Supplying both with different values is invalid. Native Responses and Messages retain the existing output-limit fallback where available. Pooled children reject any `previous_response_id` with `400 invalid_input`; hidden prior context cannot be included in the admission estimate. The [reasoning-budget validation](/platform/budget-keys#reasoning-budgets-stay-under-the-output-ceiling) for budget keys runs before pool reservation, using the original output ceiling before any reservation overhead. Existing child output-parameter validation, single-completion restriction, model restrictions, and thinking-off rules remain in force.

## Errors

Errors use the existing v2 `error` envelope. Inspect `error.details.reason` and `retryable`. HTTP 402 uses coarse code `INSUFFICIENT_CREDIT`, HTTP 503 uses `INTERNAL_ERROR`, and the other budget errors use `HTTP_EXCEPTION`.

| HTTP | Reason                                                                        | Meaning                                                                                                                                |
| ---- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| 400  | `invalid_input`                                                               | Invalid field, amount, expiry, cursor, unsupported translated format, or hidden response history. Unknown request fields are rejected. |
| 400  | `max_tokens_required`                                                         | A required output ceiling is missing.                                                                                                  |
| 400  | `model_not_poolable`                                                          | The resolved model is outside the pool allowlist.                                                                                      |
| 401  | `budget_key_expired`, `budget_key_revoked`                                    | The child cannot accept new requests.                                                                                                  |
| 403  | `budget_key_route_forbidden`                                                  | A child or session attempted management.                                                                                               |
| 403  | `budget_pool_management_forbidden`, `budget_key_management_forbidden`         | The caller is not authorized by the target's management policy.                                                                        |
| 404  | `budget_pool_not_found`, `budget_operation_not_found`, `budget_key_not_found` | The scoped resource is absent or belongs to another organization.                                                                      |
| 402  | `budget_exhausted`                                                            | The pool or optional child cap cannot cover the request.                                                                               |
| 402  | `budget_pool_paused`                                                          | Admission is paused; details include `pause_reason`.                                                                                   |
| 409  | `budget_pool_conflict`, `budget_key_mint_conflict`                            | A create or mint identity was reused with different parameters.                                                                        |
| 409  | `budget_operation_conflict`, `budget_mutation_conflict`                       | An operation or mutation ID was reused with different parameters.                                                                      |
| 409  | `budget_pool_generation_mismatch`                                             | A write targeted the wrong generation.                                                                                                 |
| 409  | `budget_pool_version_conflict`                                                | `If-Match` is stale.                                                                                                                   |
| 409  | `budget_pool_children_open`                                                   | Close requires children to be closed or `force: true`.                                                                                 |
| 409  | `budget_pool_closing`, `budget_pool_closed`                                   | The pool no longer accepts the requested mutation.                                                                                     |
| 409  | `budget_pool_not_resumable`                                                   | Liabilities exceed authorization or the pool is too old.                                                                               |
| 503  | `budget_store_unavailable`, `budget_state_unknown`                            | Dependencies or recovery cannot establish reliable accounting. Retry with the same identity.                                           |
| 503  | `budget_pricing_unavailable`                                                  | Required admission pricing is unavailable.                                                                                             |
| 503  | `budget_pools_disabled`                                                       | New pool admission is disabled for this organization; automatic retry or downgrade is inappropriate.                                   |

`budget_exhausted` details include the same atomic decision's `authorized_usd`, `committed_usd`, `in_flight_usd`, and `estimate_usd`, plus `scope: pool|child`. A child-bound refusal also includes `spent_usd`, `reserved_usd`, and `cap_usd`. `blocking: in_flight` means outstanding reservations are the blocker and `retryable` is true. `blocking: committed` means waiting for reservations alone will not make the request fit. Operation refusals include `operation_id`.

## Recovery and limits

Pool execution is temporary: idle timeout is 24 hours and maximum age is seven days. It is not a permanent wallet. Your application coordinates wallet updates, generation changes, external work, and which wallet events are already included in a new pool's initial authorization. Sunra does not implement funding buckets, subscriptions, order state, or wallet shortfall compensation. Wallet compensation does not automatically grant new pool authorization.

The supported-model contract targets an output ceiling that includes all billed output, including reasoning where declared. Admission still uses the existing conservative prompt-byte estimate and reserves the full output ceiling. Each recognized image, audio, video, or document part contributes 8,192 input tokens: a dedicated media price is used when present, otherwise the allowance is folded into prompt pricing. These input conventions can reject requests that would have cost less. A small positive available amount does not guarantee admission; lower a suitable output ceiling or wait for in-flight work to settle. Actual paid amounts are retained rather than truncated to authorization. A deliberate downward authorization adjustment can leave previously accepted responsibility above the new authorization.

Strict actual-cost coverage requires acceptance verification of provider behavior and input/media accounting. The initial acceptance record for `qwen3.5-plus` contains 66 billed output tokens at a requested ceiling of 64. By the September 19 admission decision, pool reservations for this model price the admitted ceiling plus two completion tokens (64 reserves 66). The ceiling sent upstream and the client body remain unchanged. Other models have zero completion overhead. Local tests verify this reservation behavior, not a new live-provider measurement.

Redis state loss stops new spending. Recovery reconstructs paid spend, applied external operations, and open completion reservations from durable records, then leaves the pool paused for an explicit resume. Open journal entries retain their financial responsibility even after the request lease expires. Only an expired reservation with no completion row can be reclaimed as an undispatched orphan.

If Redis disappears while an external operation or lifecycle decision is still awaiting durable confirmation, its exact decision may be unknowable. The operation remains pending and the pool fails closed until that evidence can be resolved; it is never blindly debited or credited again. A timeout or a finalizing close is not proof of zero cost. Contact Sunra with the pool and operation IDs if recovery remains pending.

Client disconnects use the existing cancellation and persisted settlement-evidence path. Receipts describe durable Sunra billing records; they do not invent usage that was never recorded. New provider routes require renewed boundary verification, and the measured model matrix is not a guarantee that future provider implementations will preserve today's behavior.

Operators can disable new pool admission independently of reads, settlement, credit repair, and close. All auth and gateway instances must understand pooled credentials before enabling the feature. Rollback must first stop admission and finish existing financial responsibility; it must preserve readers for retained receipts and operations.

The deployment gates are `BUDGET_POOLS_ENABLED` (default `false`), `BUDGET_POOLS_ORG_ALLOWLIST` (comma-separated organization IDs; empty grants none), and `BUDGET_POOLS_ADMISSION_ENABLED` (set to `false` to stop new admissions; otherwise enabled subject to the first two gates). Configure the same gates on auth and apiv1. Existing unpooled credentials retain their existing contract when these gates are off.
