---
title: "Spend refusals"
description: "The object every budget, quota and tree-limit refusal carries, so an agent or an app can wait, ask or stop without reading the message."
canonical_url: "https://docs.oz0.ai/reference/spend-refusals"
---
# Spend refusals

> The object every budget, quota and tree-limit refusal carries, so an agent or an app can wait, ask or stop without reading the message.

For: Developers handling budget and quota refusals.

A budget, quota or job-tree limit refusal adds an `oz0` object beside the error's `type` and `message`. Use the object's fields to decide whether to wait, request more budget or stop. The message is for people and its wording can change.

```json
{
  "type": "error",
  "error": {
    "type": "oz0_quota_exceeded",
    "message": "team ml has spent $41.2000 of its $40.00 quota for October 2026; ask for more ...",
    "oz0": {
      "v": 1,
      "scope": "team",
      "scope_id": "team:ml",
      "window": "month",
      "resets_at": "2026-11-01T00:00:00Z",
      "limit_usd": 40,
      "spent_usd": 41.2,
      "call_estimate_usd": 0.0912,
      "limit_name": "monthly_usd",
      "can_wait": true,
      "raise": {
        "by": ["team_admin", "tenant_admin"],
        "command": "oz0 quota request team:ml --usd 10 --tenant acme --reason '...'"
      }
    }
  }
}
```

A client branches on the fields: it waits until `resets_at` (or for `Retry-After`), follows `raise.request` once a budget request is open, or stops.

## Which refusals carry it

<table>
<thead>
  <tr>
    <th>
      Error type
    </th>
    
    <th>
      Status
    </th>
    
    <th>
      What
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        oz0_budget_exceeded
      </code>
    </td>
    
    <td>
      402
    </td>
    
    <td>
      A job tree's budget or the tenant's monthly budget is spent
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        oz0_quota_exceeded
      </code>
    </td>
    
    <td>
      402 at the gateway, <code>
        resource_exhausted
      </code>
      
       (429) at the runtime API
    </td>
    
    <td>
      A person's, a key's, a service's or a team's quota: money a month, jobs a day, or jobs at once
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        oz0_budget_wait
      </code>
    </td>
    
    <td>
      402
    </td>
    
    <td>
      A job tree's budget is spent and the job waits for a person to raise it (<code>
        on_budget: wait
      </code>
      
      )
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        oz0_budget_forecast
      </code>
    </td>
    
    <td>
      402
    </td>
    
    <td>
      A forecast asks a person before the tree reaches its budget
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        oz0_tree_limit
      </code>
    </td>
    
    <td>
      402
    </td>
    
    <td>
      A job tree's limit that is not money: model calls, tokens, children, children at once, depth, or an agent's delegation limits
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        oz0_budget_busy
      </code>
    </td>
    
    <td>
      429
    </td>
    
    <td>
      The call does not fit only because other calls of the same budget still run; send it again when they settle
    </td>
  </tr>
</tbody>
</table>

`oz0_budget_unbounded` (400) carries no object either: under `reserve: strict` the call's worst case [cannot be bound](https://docs.oz0.ai/build/parts/contracts-and-limits/budgets#each-call-reserves-its-worst-case), or under any mode it comes to more than a reservation can hold. No budget's figures are involved, and sending it again changes nothing.

`oz0_budget_unavailable` (503) carries no object: the budgets could not be read, which is an outage, not a refusal, and the call is sent again shortly. Neither does `oz0_pool_cooling` (429), a provider's cooldown, or `oz0_killed`.

## Where it appears

<table>
<thead>
  <tr>
    <th>
      Surface
    </th>
    
    <th>
      Where the object is
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      The gateway (<code>
        /llm/...
      </code>
      
      ), a node's local model call and a child job's admission
    </td>
    
    <td>
      <code>
        error.oz0
      </code>
      
       of the answer's body
    </td>
  </tr>
  
  <tr>
    <td>
      The runtime API's <code>
        StartJob
      </code>
    </td>
    
    <td>
      A <code>
        oz0.runtime.v1.SpendRefusal
      </code>
      
       detail of the <code>
        resource_exhausted
      </code>
      
       error, with the object's fields
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        GetJob
      </code>
      
       and <code>
        WatchJob
      </code>
    </td>
    
    <td>
      <code>
        Job.refusal
      </code>
      
       of a job a budget, quota or limit ended, and <code>
        BudgetWait.refusal
      </code>
      
       of a job that waits for a budget decision
    </td>
  </tr>
  
  <tr>
    <td>
      A flow's webhook
    </td>
    
    <td>
      The 429's body, <code>
        error.oz0
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      The MCP server
    </td>
    
    <td>
      <code>
        _meta["ai.oz0/refusal"].oz0
      </code>
      
       of the <code>
        isError
      </code>
      
       tool result, and <code>
        structuredContent.error.oz0
      </code>
      
       too when the tool declares no <code>
        outputSchema
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      A tool's <a href="https://docs.oz0.ai/build/parts/tools/model-calls#mcp-sampling">
        MCP sampling request
      </a>
    </td>
    
    <td>
      <code>
        error.data["ai.oz0/refusal"].oz0
      </code>
      
       of the JSON-RPC error the tool's server gets (code <code>
        -1
      </code>
      
      ); on MCP's newer protocol, where the refused request ends the tool call, the agent's tool error carries the same object as <code>
        structuredContent.error.oz0
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      A2A
    </td>
    
    <td>
      An item <code>
        {"@type": "type.googleapis.com/google.protobuf.Struct", "value": {"error": {...}}}
      </code>
      
       of the JSON-RPC error's <code>
        data
      </code>
      
      , and a data part <code>
        {"error": {...}}
      </code>
      
       of a failed or budget-waiting task's status message
    </td>
  </tr>
  
  <tr>
    <td>
      <a href="https://docs.oz0.ai/build/apps/ag-ui">
        AG-UI
      </a>
    </td>
    
    <td>
      <code>
        RUN_ERROR
      </code>
      
      : <code>
        code
      </code>
      
       is the error type and <code>
        rawEvent.oz0
      </code>
      
       the object
    </td>
  </tr>
  
  <tr>
    <td>
      The <a href="https://docs.oz0.ai/govern/audit-trail">
        audit trail
      </a>
    </td>
    
    <td>
      <code>
        refusal_detail
      </code>
      
       of each <code>
        run.refused
      </code>
      
       entry, beside the clipped <code>
        reason
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      The evidence record of a model call
    </td>
    
    <td>
      <code>
        refusal_detail
      </code>
      
      , without the name of a person or team it is about
    </td>
  </tr>
</tbody>
</table>

A remote agent's session learns it from the answer to its usage reports, in the `Oz0-Over-Budget-Detail` header beside `Oz0-Over-Budget`.

## Fields

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Meaning
    </th>
    
    <th>
      Absent when
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        v
      </code>
    </td>
    
    <td>
      number
    </td>
    
    <td>
      The object's version: 1
    </td>
    
    <td>
      Never
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        scope
      </code>
    </td>
    
    <td>
      string
    </td>
    
    <td>
      What was spent or reached: <code>
        tree
      </code>
      
      , <code>
        tenant
      </code>
      
      , <code>
        person
      </code>
      
       (a person's account, an API key, a service identity or an operator certificate), <code>
        team
      </code>
      
      , or <code>
        limit
      </code>
      
       (a tree's limit that is not money)
    </td>
    
    <td>
      Never
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        scope_id
      </code>
    </td>
    
    <td>
      string
    </td>
    
    <td>
      The tree's root job (<code>
        tree
      </code>
      
      , <code>
        limit
      </code>
      
      ), or the principal or team (<code>
        team:&lt;name>
      </code>
      
      ) whose quota it is
    </td>
    
    <td>
      For <code>
        tenant
      </code>
      
      , and when withheld
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        window
      </code>
    </td>
    
    <td>
      string
    </td>
    
    <td>
      The period the scope counts in: <code>
        job
      </code>
      
       (the tree, which never resets), <code>
        month
      </code>
      
       (the calendar month in UTC), <code>
        day
      </code>
      
       (the UTC day) or <code>
        at_once
      </code>
      
       (jobs running at the same time)
    </td>
    
    <td>
      Never
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        resets_at
      </code>
    </td>
    
    <td>
      string or null
    </td>
    
    <td>
      When the window starts again, in RFC 3339
    </td>
    
    <td>
      Always present; <code>
        null
      </code>
      
       for a tree, a limit and jobs at once
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        limit_usd
      </code>
    </td>
    
    <td>
      number
    </td>
    
    <td>
      What the scope may spend, in US dollars
    </td>
    
    <td>
      For limits and quotas on jobs, and when withheld
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        spent_usd
      </code>
    </td>
    
    <td>
      number
    </td>
    
    <td>
      What the scope has spent
    </td>
    
    <td>
      As <code>
        limit_usd
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        reserved_usd
      </code>
    </td>
    
    <td>
      number
    </td>
    
    <td>
      What calls of the scope still running hold at their worst case
    </td>
    
    <td>
      When nothing is held, and when withheld
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        call_estimate_usd
      </code>
    </td>
    
    <td>
      number
    </td>
    
    <td>
      The refused call's worst case, at which it was to be reserved
    </td>
    
    <td>
      When the refusal is not about a call (a start, a child's admission), and when withheld
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        limit_name
      </code>
    </td>
    
    <td>
      string
    </td>
    
    <td>
      The limit reached: <code>
        model_calls
      </code>
      
      , <code>
        tokens
      </code>
      
      , <code>
        children
      </code>
      
      , <code>
        fan_out
      </code>
      
      , <code>
        depth
      </code>
      
      , <code>
        wait
      </code>
      
      , <code>
        delegation_max_depth
      </code>
      
      , <code>
        delegation_max_children
      </code>
      
      , <code>
        jobs_per_day
      </code>
      
      , <code>
        jobs_at_once
      </code>
      
       or <code>
        monthly_usd
      </code>
    </td>
    
    <td>
      For budgets, and when withheld
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        steps
      </code>
    </td>
    
    <td>
      number
    </td>
    
    <td>
      A count the tree reached: model calls, child jobs, children at once or depth
    </td>
    
    <td>
      Unless <code>
        limit_name
      </code>
      
       is one of those
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        steps_limit
      </code>
    </td>
    
    <td>
      number
    </td>
    
    <td>
      The limit of <code>
        steps
      </code>
    </td>
    
    <td>
      As <code>
        steps
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        tokens
      </code>
    </td>
    
    <td>
      number
    </td>
    
    <td>
      The tree's tokens
    </td>
    
    <td>
      Unless <code>
        limit_name
      </code>
      
       is <code>
        tokens
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        tokens_limit
      </code>
    </td>
    
    <td>
      number
    </td>
    
    <td>
      The tree's limit on tokens
    </td>
    
    <td>
      As <code>
        tokens
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        seconds
      </code>
    </td>
    
    <td>
      number
    </td>
    
    <td>
      The time the tree waited for people
    </td>
    
    <td>
      Unless <code>
        limit_name
      </code>
      
       is <code>
        wait
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        seconds_limit
      </code>
    </td>
    
    <td>
      number
    </td>
    
    <td>
      The tree's limit on waiting
    </td>
    
    <td>
      As <code>
        seconds
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        jobs
      </code>
    </td>
    
    <td>
      number
    </td>
    
    <td>
      The jobs started today, or running now
    </td>
    
    <td>
      Unless <code>
        limit_name
      </code>
      
       is <code>
        jobs_per_day
      </code>
      
       or <code>
        jobs_at_once
      </code>
      
      , and when withheld
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        jobs_limit
      </code>
    </td>
    
    <td>
      number
    </td>
    
    <td>
      The quota on those jobs
    </td>
    
    <td>
      As <code>
        jobs
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        calls_running
      </code>
    </td>
    
    <td>
      number
    </td>
    
    <td>
      For <code>
        oz0_budget_busy
      </code>
      
      : the calls of the budget still running
    </td>
    
    <td>
      For every other type, and when withheld
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        forecast_chance
      </code>
    </td>
    
    <td>
      number
    </td>
    
    <td>
      For <code>
        oz0_budget_forecast
      </code>
      
      : the share of the agent's earlier trees that went on to pass the budget from here
    </td>
    
    <td>
      For every other type
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        can_wait
      </code>
    </td>
    
    <td>
      boolean
    </td>
    
    <td>
      <code>
        true
      </code>
      
       when the job waits for a person's decision instead of failing: a tree under <code>
        on_budget: wait
      </code>
      
      , a forecast, a quota refusing a job's model call
    </td>
    
    <td>
      Never
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        raise
      </code>
    </td>
    
    <td>
      object
    </td>
    
    <td>
      Who can raise the scope, and how
    </td>
    
    <td>
      Never
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        raise.by
      </code>
    </td>
    
    <td>
      array of strings
    </td>
    
    <td>
      The roles that can: <code>
        tree_owner
      </code>
      
       (the person who started the tree), <code>
        team_admin
      </code>
      
      , <code>
        tenant_admin
      </code>
    </td>
    
    <td>
      Never (may be empty)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        raise.request
      </code>
    </td>
    
    <td>
      object
    </td>
    
    <td>
      The budget request opened about the refusal
    </td>
    
    <td>
      Until one is opened: the gateway refuses before any request exists, so its answer never has one; a job's wait and its failure name it
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        raise.request.id
      </code>
    </td>
    
    <td>
      string
    </td>
    
    <td>
      The request's ID, for <code>
        oz0 quota approve
      </code>
    </td>
    
    <td>
      As <code>
        raise.request
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        raise.request.url
      </code>
    </td>
    
    <td>
      string
    </td>
    
    <td>
      The request on the web UI's Approvals page, which opens with that request marked (decided ones shown too when it is no longer pending)
    </td>
    
    <td>
      Without a public URL (<code>
        --public-url
      </code>
      
      )
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        raise.command
      </code>
    </td>
    
    <td>
      string
    </td>
    
    <td>
      The CLI command that asks for more (with the tenant it is in), sets more, or approves the request
    </td>
    
    <td>
      For an agent's own delegation limits, and when withheld
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        withheld
      </code>
    </td>
    
    <td>
      boolean
    </td>
    
    <td>
      <code>
        true
      </code>
      
       when the scope's name and figures were left out for this caller
    </td>
    
    <td>
      When nothing was left out
    </td>
  </tr>
</tbody>
</table>

Additions are fine: a client ignores fields it does not know. A renamed field, or one whose meaning changes, needs a new version, `v: 2`.

## Who sees the numbers

A person's or a team's figures and name are shown only to a caller who may read that quota today: the person themselves, for their own quota, or someone whose role holds `usage.read` and whom [works-council mode](https://docs.oz0.ai/govern/works-council-mode) lets see that figure (a team of at least the tenant's threshold of people, a service identity, or anyone once an admin turned the mode off).

For anyone else the object keeps `v`, `scope`, `window`, `resets_at`, `can_wait` and `raise` without its command, sets `withheld`, and the message names neither the quota nor its figures:

```json
{"v": 1, "scope": "team", "window": "month", "resets_at": "2026-11-01T00:00:00Z", "can_wait": true,
 "raise": {"by": ["team_admin", "tenant_admin"]}, "withheld": true}
```

Each surface filters for its own caller. The gateway answers the node that acts for a job, so its answer is filtered for the person who started the job's tree; the runtime API, MCP, A2A and AG-UI filter for whoever called them; a webhook's answer for the webhook's own identity (`service:webhook/<flow>`). A job tree's own budget, the tenant's budget and the tree's limits are not figures about people, and are shown to everyone who gets the refusal.

The audit trail's `run.refused` entry keeps the refusal as the limit wrote it, whoever was refused: its `reason` and `refusal_detail` name the quota that stopped the start or the call, with its figures, because the trail is for those who hold `audit.read`. The evidence record of a model call keeps the figures but never the name of a person or team: `scope_id` and `raise.command` are left out for `person` and `team`, as the record names only kinds of budget.

## Retry-After

A refusal of a quota or of the tenant's month whose `resets_at` is within 24 hours carries `Retry-After`, in seconds until the reset: on the gateway's 402, on a webhook's 429, and in the metadata of `StartJob`'s error. A client that honours it waits without code of its own. A tree's budget and its limits never reset, so their refusals never carry it. `oz0_budget_busy` keeps its own short `Retry-After` (two seconds), since what it waits for is calls that settle, not a reset.


## Sitemap

See the full [sitemap](https://docs.oz0.ai/sitemap.md) for all pages.
