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

# Budgets

> Set spending limits on any scope (workspace, project, identity, API key, provider, or model) to control AI costs across the organization.

See [HTTP URLs and authentication](/docs/reference/base-urls.md) and the API reference for complete request and response formats.

**Budgets** apply spending controls to a named target. Each budget defines a scope (what it applies to), period limits (how much can be spent or consumed per reset period), and an optional expiration date after which the budget stops enforcing.

When a cost or token limit is reached, requests matching the budget's scope are blocked until the period resets. Requests per minute limits block for one minute only, independent of the reset period. Multiple budgets at different levels can apply to the same request at once, and each is enforced independently; see [Budget levels and interaction](#budget-levels-and-interaction).

Common use cases:

* Cap monthly spend per customer or tenant by scoping a budget to an **Identity**.
* Prevent a staging or CI environment from consuming production credits by scoping a budget to its **API Key**.
* Limit exposure to an expensive provider or model during evaluation by scoping a budget to a **Provider** or **Model**.
* Enforce a workspace-wide cost ceiling as a safety net by scoping a budget to the **Workspace**. Usage is settled after each request completes, so requests in flight when a limit is reached can still complete.
* Control spend for a time-boxed experiment using a **One-time** reset period with an expiration date.

Navigate to **Settings > Organization > Budgets** to manage budgets.

> [!NOTE]
>
> In the console, only **Admin** members can create, edit, or delete budgets. See Members and Teams for the roles. Through the API, budgets are managed with a Management Key that has the Budgets permission; project-scoped API keys cannot manage budgets.

## Overview

The Budgets table lists all budgets in the organization.

| Column | Description |
| - | - |
| **Budget** | The name identifying the budget. |
| **Scope** | The type of entity the budget applies to: Workspace, Project, Identity, API Key, Provider, or Model. |
| **Target** | The entity itself, such as the workspace, a project name, an API key name, an identity name, a provider, or a model name. |
| **Limits** | The configured cost and token limits for the reset period. Click a budget row to [see current usage](#monitor-usage). |
| **Reset** | The configured reset period: Daily, Weekly, Monthly, Yearly, or One-time (no automatic reset). |
| **Expires** | Time remaining until the budget stops enforcing. Shown in red when expiration is approaching. After expiration the budget remains in the list but no longer enforces. Blank if the budget never expires. |
| **Updated** | The date the budget was last modified. |

Use <kbd> Filter</kbd> to narrow the list by **Period** (Daily, Monthly, One-time, Weekly, Yearly), **Scope**, or **Status** (Active, Expired).

Use <kbd> Sort by</kbd> to reorder by **Expiry soon**, **Newest**, or **Recently updated**.

### Monitor usage

Click a budget row to open its detail page:

* A metric card for each configured limit (**Total spend**, **Tokens**, **Req per minute**) shows current usage, a progress bar, and percentage consumed.
* **Scope**, **Resets** period, **Renews in** countdown, and **Expires** date appear at the top.

- Click <kbd>Adjust limits</kbd> to edit the budget's period limits or expiration date.
- Click <kbd> Add threshold</kbd> in the **Alerts** section to [set up an alert](#create-an-alert).
- Click <kbd>Delete Budget</kbd> to permanently remove it. Enforcement stops immediately.

## Create a budget

**1. Open the creation dialog**

Click <kbd> Budget</kbd> in the top-right corner of the Budgets page.

**2. Set the scope**

Under **Scope**, select what the budget **Applies to**:

| Option | Description |
| - | - |
| **Workspace** | Applies to the entire workspace. |
| **Project** | Applies to a specific project. |
| **Identity** | Applies to a contact by external ID. |
| **API Key** | Applies to a specific API key. |
| **Provider** | Applies to all requests to a provider. Matches the provider segment of the model reference (`openai` in `openai/gpt-5.6-sol`). |
| **Model** | Applies to a specific model, matched by the full model reference as sent in the request (`openai/gpt-5.6-sol`). |

For all scope types except **Workspace**, a second field appears to select the specific target (for example, which API key or which identity).

> [!NOTE]
>
> Only one budget is allowed per target. If a budget already exists for the selected target, an inline error is shown and the budget cannot be created until a different target is selected.

See [Budget levels and interaction](#budget-levels-and-interaction) for how budgets at different levels apply to the same request.

**3. Configure period limits**

Under **Period limits**, set the **Reset period** (required) and one or more of the following limits:

| Limit | Unit | Description |
| - | - | - |
| **Cost** | USD | Maximum spend allowed per reset period. |
| **Tokens limit** | K Tokens | Token cap per reset period, in thousands. The API takes whole tokens in `limits.token_limit`. |
| **Requests per minute** | Req/min | Rate cap enforced every minute, at all times. |

Leave a limit at `0` to leave it unconfigured. At least one limit must be set to a non-zero value to create the budget.

Reset periods follow a fixed schedule:

| Period | Resets at |
| - | - |
| Daily | Midnight UTC |
| Weekly | Monday midnight UTC |
| Monthly | 1st of the month, midnight UTC |
| Yearly | January 1st, midnight UTC |
| One-time | Does not reset. Limit applies for the lifetime of the budget. |

> [!NOTE]
>
> **Requests per minute** is a fixed 60-second window that starts with the first request, independent of the reset period. See [Budget limits](/docs/ai-gateway/features/rate-limits.md#budget-limits).

> [!TIP]
>
> If a budget is exhausted before the period resets, an admin can increase its limit to unblock further usage immediately. Existing usage is not removed; the higher limit simply allows the entity to consume more within the current period.

**4. Set an expiration date (optional)**

Under **Expiration**, set an **Expiration date** if the budget should stop enforcing after a specific date. Leave it as **Never expires** for an indefinite budget.

**5. Create the budget**

Click <kbd>Create budget</kbd>. The budget appears in the list immediately and begins enforcing on the next request.

## Edit or delete a budget

Select the <kbd /> menu on any budget row to:

* **Edit**: update the period limits or expiration date. The scope and target cannot be changed after creation.
* **Delete**: permanently removes the budget. Enforcement stops immediately.

Budgets can also be managed programmatically with a **Management Key**:

* [Create a budget](/docs/reference/budgets/create-a-new-budget.md)
* [Update a budget](/docs/reference/budgets/update-a-budget.md)
* [Retrieve a budget](/docs/reference/budgets/retrieve-a-budget.md)
* [List budgets](/docs/reference/budgets/list-budgets.md)
* [Delete a budget](/docs/reference/budgets/delete-a-budget.md)
* [Reset budget consumption](/docs/reference/budgets/reset-budget-consumption.md)

Regular API keys cannot manage Budgets.

## Configure alerts

**Alerts** notify configured notifiers when a budget's cost usage crosses a percentage threshold.

The gateway performs two separate checks:

* **Before a request starts**, the gateway verifies the budget hasn't been fully consumed. Requests are blocked once usage reaches 100%.
* **After each request completes**, it evaluates alert thresholds against the updated usage, so a notification can lag slightly behind the moment the threshold was crossed.

Each alert fires at most once per period and re-arms on reset or a limit change.

**Example:** An 80% cost threshold on a project's budget, notifying the project administrator by email, gives time to react before the budget blocks traffic at 100%.

> [!NOTE]
>
> A budget alert fires at a percentage of a limit.
>
> * Alerts track the cost dimension (`BUDGET_ALERT_DIMENSION_COST`) only, so a cost limit must be set before <kbd>Add threshold</kbd> becomes available.
> * A `BUDGET_ALERT_DIMENSION_TOKENS` alert is rejected.
> * Requests-per-minute limits do not trigger alerts.
>
> For alerts on latency, errors, or guardrail results, see the AI Studio [**Alerts**](/docs/ai-studio/observability/alerts.md) page.

### Create a notifier

**Notifiers** must exist before they can be attached to an alert. Create one under **Settings** → **Organization** → **Notifiers**, from the alert form itself, or with the [**Notifiers API**](/docs/reference/notifiers/create-a-notifier.md). See [Notifiers](/docs/ai-studio/observability/notifiers.md) for the channels, the destination fields, and the walkthrough in **AI Studio**.

### Create an alert

The **Alerts** section is on the budget's create/edit panel, and on its detail page (open a budget by clicking its row in the Budgets list).

**1. Add a threshold**

In the **Alerts** section, click <kbd> Add threshold</kbd>.

**2. Set the threshold percentage**

Enter a whole number between 1 and 100. Each budget allows only one alert per threshold percentage; duplicate thresholds are rejected.

**3. Add notifiers**

Click <kbd>Add notifiers</kbd> and search for one or more already-created notifiers to receive the alert. At least one notifier is required. Each alert allows a maximum of 10 notifiers.

**4. Save the alert**

Click <kbd>Add alert</kbd> (or <kbd>Save changes</kbd> when editing an existing alert).

Existing alerts appear as rows in the **Alerts** section. Select <kbd /> to edit a threshold or its notifiers, or <kbd /> to delete the alert.

## Examples

### Send notifications to a Microsoft Teams channel

Route alert notifications to a **Microsoft Teams** channel with a generic webhook notifier. Teams renders messages in its own card format, so the webhook points at a small relay that converts **Toqan Router**'s notification payload before forwarding it to the channel.

**1. Create the Teams incoming webhook**

In the target **Microsoft Teams** channel, open **Workflows** (or **Power Automate**), search for the **Post to a channel when a webhook request is received** template, complete the setup, and copy the webhook URL it generates.

**2. Deploy a payload relay**

**Toqan Router** sends the raw notification envelope as JSON, and Teams only accepts its own message card format, so the webhook cannot point at the Teams URL directly. Deploy a small relay that converts the payload:

> [accordion] Relay code
>
> ```javascript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
> export default {
>   async fetch(request, env) {
>     if (request.method !== "POST") {
>       return new Response("Method not allowed", { status: 405 });
>     }
>
>     let envelope;
>     try {
>       envelope = await request.json();
>     } catch {
>       return new Response("Invalid JSON body", { status: 400 });
>     }
>     const links = envelope.links ?? [];
>
>     const card = {
>       "@type": "MessageCard",
>       "@context": "http://schema.org/extensions",
>       themeColor: severityColor(envelope.severity),
>       title: envelope.title,
>       text: envelope.body,
>       sections: [
>         {
>           facts: [
>             { name: "Severity", value: envelope.severity },
>             ...links.map((link) => ({ name: link.text, value: link.url })),
>           ],
>         },
>       ],
>     };
>
>     const teamsResponse = await fetch(env.TEAMS_WEBHOOK_URL, {
>       method: "POST",
>       headers: { "Content-Type": "application/json" },
>       body: JSON.stringify(card),
>     });
>
>     if (!teamsResponse.ok) {
>       // Relay the status so Toqan Router retries the delivery.
>       console.log(`Teams rejected the message: ${teamsResponse.status}`);
>       return new Response(`Teams rejected the message: ${teamsResponse.status}`, {
>         status: teamsResponse.status,
>       });
>     }
>
>     console.log(`Forwarded notification ${envelope.id} to Teams (${teamsResponse.status})`);
>     return new Response("OK", { status: 200 });
>   },
> };
>
> function severityColor(severity) {
>   switch (severity) {
>     case "critical":
>       return "B71C1C";
>     case "warning":
>       return "F57C00";
>     case "ok":
>       return "2E7D32";
>     default:
>       return "1565C0";
>   }
> }
> ```

Set `TEAMS_WEBHOOK_URL` to the webhook URL from the previous step. The example deploys as a Cloudflare Worker; any HTTP endpoint that accepts JSON works.

**3. Create the webhook notifier**

Under **Settings** → **Organization** → **Notifiers**, click <kbd> Notifier</kbd>, enter a **Name**, select **Webhook** as the **Send via** channel, and paste the relay URL as the **Webhook URL**. Custom request headers are optional.

To create the notifier through the API:

```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl --location 'https://router.toqan.ai/eu/v2/notifiers' \
--header "Authorization: Bearer $TOQAN_API_KEY" \
--header 'Content-Type: application/json' \
--data-raw '{
    "display_name": "Teams alerts",
    "type": "NOTIFIER_TYPE_WEBHOOK",
    "webhook_url": "https://teams-relay.example.workers.dev"
}'
```

See the [API reference](/docs/reference/notifiers/create-a-notifier.md) for the full parameter specification.

**4. Attach the notifier to an alert**

Add the notifier to a [budget alert](#create-an-alert) under **Add notifiers**, or to an **Observability** [Alert](/docs/ai-studio/observability/alerts.md) under **Notify via**. Each alert accepts up to 10 notifiers.

**5. Test delivery**

Create a budget with a small cost limit and an alert at a low threshold, then make a request that crosses it. The alert fires on the next check and the message lands in the channel. If nothing arrives, check the relay logs first: they record every converted payload, including any rejection from Teams.

**Payload format**: **Toqan Router** POSTs the notification envelope as `application/json` with `X-Orq-Hook-ID`, `X-Orq-Event`, and `Idempotency-Key` headers, plus any headers configured on the notifier:

```json JSON theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "id": "ntf_01JQ8Z2K4M6P8R0T2V4X6Z8B0D",
  "version": 1,
  "workspace_id": "ws_01JQ8Z0MB5W1YTVN3GCK7B0EXA",
  "timestamp": "2026-08-08T09:30:00Z",
  "title": "Budget usage has reached your notification threshold",
  "body": "Your usage today for workspace acme has reached $92.00 (92%) of your $100.00 budget. This alert is configured at 90% ($90.00).",
  "severity": "warning",
  "links": [
    { "text": "Manage budgets", "url": "https://router.toqan.ai/eu/budgets" }
  ],
  "tags": {
    "budget_id": "bgt_01JQ8Z0MB5W1YTVN3GCK7B0EXA",
    "scope_kind": "WORKSPACE",
    "period": "DAILY",
    "threshold_percent": "90"
  }
}
```

Teams rejects this envelope with HTTP `400` because it expects a message card with `@type` and `@context`. The relay converts the envelope into that format before forwarding.

**Troubleshooting**:

| Symptom | Cause |
| - | - |
| Teams responds with `400` | The relay is missing or misconfigured. Confirm the relay actually maps the envelope to a MessageCard before checking anything else. |
| Nothing arrives in the channel | The webhook URL is stale or revoked, or the relay crashed. Check the relay logs for the delivered and rejected requests. |
| Notifications arrive late | Non-`2xx` responses are retried, so a failing relay delays delivery until it recovers. |

## Budget levels and interaction

Budgets can be set at six levels: **Workspace**, **Project**, **Identity**, **API Key**, **Provider**, and **Model**.

When several budgets apply to the same request, every one of them is enforced independently and cumulatively. There is no precedence: a budget at one level never overrides a budget at another level, and a request must satisfy every budget that matches it.

Because only one budget is allowed per target (see [Create a budget](#create-a-budget)), a request can match at most one scoped budget per level, up to six scoped budgets in total, plus every budget whose [match expression](#scope-by-expression-api) it satisfies. When a budget is exhausted, only requests that match it are rejected.

### Scope by expression (API)

The API also accepts a `match` CEL expression in place of a structured scope, for targets the six levels do not cover. The expression is evaluated against the request context, and the budget applies when it returns true. `match` and `scope` are mutually exclusive.

```json JSON theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "match": { "cel": "metadata.team == \"ml\" && provider == \"openai\"" },
  "limits": { "period": "BUDGET_PERIOD_MONTHLY", "amount": 100 }
}
```

Available variables: `model`, `provider`, `model_id`, `api_key`, `identity`, `project`, `metadata` (map), and `headers` (map, lowercase keys). Send either `match` or `scope`, never both: an empty expression is rejected unless the budget is created from a scope. A workspace-wide budget stores the expression that matches every request in the workspace.

### How multiple budgets interact

* **Every applicable budget is enforced.** A request must pass every budget that matches it. No budget overrides another.
* **Each budget tracks its own usage.** A request counts toward every budget that applies to it, at the same time.
* **The most restrictive budget binds.** The effective cap for a dimension is the most restrictive applicable budget. Limits on different dimensions (cost, tokens, requests per minute) all apply simultaneously.
* **Requests per minute are enforced per budget**, each with its own 60-second window that starts with the first request.
* **Expired budgets stop enforcing.** After the expiration date a budget no longer blocks requests, even though it stays in the list.

### What counts toward a budget

Usage settles into every budget that matches a request after **Toqan Router** completes it. Cost counts at the model's list price; tokens count from the provider's usage.

| Source | Counts | Note |
| - | :-: | - |
| **AI Gateway** model calls | Yes | Chat, Responses, embeddings, images, audio, classify, OCR, rerank, batches, and web search settle cost and tokens into every matching budget. |
| Agents and Deployments | Yes | Model calls made by an Agent or Deployment run settle like any other Gateway request. |
| Evaluator LLM-judge calls | Yes | Judge calls check the budgets that match their model and scope, and settle the same way. |
| BYOK and private models | Yes | Tokens count, and cost counts at the model's list price even though the request draws no credits. |
| Experiments | Partly | Budgets do not gate the experiment run itself, but model calls made by its Agent tasks settle into workspace, project, provider, and model budgets. Identity and API-key budgets receive no experiment usage. |
| OpenTelemetry and SDK-ingested spans | No | **Toqan Router** did not proxy the call, so the span is observability data and does not settle usage. |

### Why a budget can show \$0 while traces show cost

| Symptom | Cause |
| - | - |
| Usage stays at \$0.00 | The traffic is outside the budget's scope. Check the budget's Project, Identity, API key, Provider, or Model target against the trace. |
| Experiment usage appears on a workspace, project, provider, or model budget but not on an Identity or API key budget | Experiment runs carry no identity or API-key attribution, so those scopes record nothing. The same model calls settle into the other matching budgets. |
| Usage stays at \$0.00 for OpenTelemetry or SDK-ingested spans | Those spans describe calls **Toqan Router** did not proxy, so they do not settle usage. |
| Usage stays at \$0.00 after creating a budget | Usage starts with the next matching request. Spend that happened before the budget was created is not counted. |
| Usage returns to \$0.00 | The reset period rolled over. The card shows usage for the current period only. |
| A trace shows cost while the budget card shows \$0 | Spend below one cent rounds to `$0` on the card. Hover the value to see the exact decimals. |

### Worked examples

> [accordion] Workspace budget plus a per-project budget
>
> The workspace has a \$1,000/month budget. The **Production** project has a \$300/month budget.
>
> * Requests to Production count toward both budgets; requests to other projects count only toward the workspace budget.
> * When Production reaches \$300, requests to Production are rejected until the monthly reset. Other projects keep working until the workspace budget is exhausted at \$1,000.

> [accordion] Per-identity budget inside a project with its own budget
>
> The **Support** project has a \$500/month budget. The identity `customer-acme` has a \$50/month budget.
>
> * The identity budget applies across all projects: the \$50/month cap is enforced whenever `customer-acme` makes a request, regardless of project.
> * Requests from `customer-acme` count toward both budgets. When the identity budget is exhausted, those requests are rejected even though the project budget has room; other identities keep working.

> [accordion] Per-model budget alongside a workspace-wide cap
>
> The workspace has a \$2,000/month budget. The model `anthropic/claude-sonnet-5` has an \$800/month budget.
>
> * When the model budget is exhausted, requests to that model are rejected; other models keep working.
> * When total workspace spend reaches \$2,000, all requests are rejected until the monthly reset, regardless of model.

### When a limit is exceeded

A request that matches an exhausted budget is rejected with `429 Too Many Requests`. The response identifies the budget and the exceeded dimension (`cost`, `tokens`, or `requests`); the response headers report the limit, remaining usage, and seconds until reset. Only requests that match the exhausted budget are blocked.

| Dimension | Behavior when exceeded |
| - | - |
| **Cost** and **Tokens** | Counters update as requests complete. When a budget reaches 100%, the next matching request is rejected. Requests stay blocked until the budget's period resets, or an admin raises the limit. |
| **Requests per minute** | Requests are rejected for the rest of the current 60-second window, independent of the reset period. |

For budgets with a **One-time** reset period, there is no automatic reset: matching requests stay blocked until an admin raises the limit, deletes the budget, or its expiration date passes.

### Reset periods

Each budget has its own reset period and its own counters. Budgets at different levels can use different periods: **Daily**, **Weekly**, **Monthly**, **Yearly**, or **One-time** (no reset). See [Create a budget](#create-a-budget) for the reset schedule.

## See also

> [card] [Rate limits & quotas](/docs/ai-gateway/features/rate-limits.md)
>
> What the **AI Gateway** enforces itself versus upstream providers, and the 429 response contract.

> [card] [API Keys](/docs/ai-gateway/configuration/api-keys.md)
>
> Create and manage AI Gateway API keys with optional spending limits.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.