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

# Budgets

> Cap spend per organization, member, team, API key, or end user with daily, weekly, or monthly limits, threshold alerts, and 403 enforcement.

A budget caps what one scope can spend in a period. When spend crosses an `alert` threshold the platform records an event and posts to your webhook. When it crosses a `block` threshold the gateway rejects further billable requests with `403 budget_exceeded` until the period resets.

Budgets count spend from every billable service on the platform, priced at the rate in effect when each request ran. Enforcement is best-effort and fail-open: when the budgeting service is unreachable, the gateway lets the request through. Size limits with margin.

This guide caps one end user, sets a default cap for every end user, caps the whole organization, and shows what a blocked request returns.

The [`mka1` CLI](/docs/cli/commands) has no budgets commands, so this guide has no CLI tab.

## Before you start

| Requirement                     | Notes                                                                                                                                                                                                      |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| API key with the Budgets scopes | `read:budgets` and `write:budgets` are admin-only scopes. Grant them in the key form under **Access → API Keys**.                                                                                          |
| An admin role                   | Organization admins manage their own organization's budgets (`owner: "org"`). Cluster admins can also set operator ceilings and target other organizations; see [For cluster admins](#for-cluster-admins). |
| The cluster currency            | Limits are in major units of the cluster currency, with no conversion: a limit of `5` means 5 of whatever [Step 1](#step-1---read-the-cluster-currency) returns.                                           |

## Scopes

Every budget targets one scope. The `id` you pass is the ID of the thing being capped.

| Scope                                                   | Caps                                                                                    | `id`                                    | Who manages it                                                                                                                                                                |
| ------------------------------------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `org`                                                   | Everything the organization spends                                                      | none (the caller's organization)        | Organization admins set the self-budget; cluster admins set the ceiling                                                                                                       |
| `apikey`                                                | One API key                                                                             | The key ID                              | Over the API, cluster admins, and organization admins for keys in their own organization. In the console, cluster admins only; see [For cluster admins](#for-cluster-admins). |
| `user`                                                  | One member of the organization                                                          | The member's user ID                    | Organization admins for their own organization; cluster admins for any                                                                                                        |
| `team`                                                  | One team                                                                                | The team ID                             | Same as `user`                                                                                                                                                                |
| `external_user`                                         | One end user you attribute requests to with `X-On-Behalf-Of`                            | The end-user ID you send in that header | Same as `user`                                                                                                                                                                |
| `user_default`, `team_default`, `external_user_default` | Each member, team, or end user that has no explicit budget of the same owner and period | none                                    | Same as `user`                                                                                                                                                                |

A default is a rule, not a counter. It caps each member, team, or end user against their own spend, so alerts and blocks fire per target, and it has no spend gauge of its own. An explicit budget for one target replaces the default of the same owner and period for that target, whether it raises or tightens the cap. Delete the explicit budget and the default applies again.

Every budget also has an `owner`. `org` is a self-budget the organization sets on itself. `cluster` is an operator ceiling that only a cluster admin can set or change; see [For cluster admins](#for-cluster-admins). One budget exists per scope and owner, so a scope can carry both, and a request has to pass every budget that applies to it. The most restrictive one wins.

## What a budget holds

| Field                           | Values                                         | Notes                                                                                                                                                                                                                                                                 |
| ------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `period`                        | `daily`, `weekly`, `monthly`                   | Spend windows reset on UTC calendar boundaries. The live gauge names the current window, for example `2026-07-07`, `2026-W28`, or `2026-07`.                                                                                                                          |
| `limit`                         | number greater than 0                          | Major units of the cluster currency.                                                                                                                                                                                                                                  |
| `thresholds`                    | one or more `{ "pct": 80, "action": "alert" }` | `pct` is an integer from 1 to 100, a percentage of the limit. An `alert` threshold fires the webhook once per period; a `block` threshold returns `403` until the period resets.                                                                                      |
| `webhook_url`, `webhook_secret` | optional                                       | `webhook_url` must be an absolute URL. Alert thresholds `POST` to it, signed with an HMAC of the secret. When editing, omit both to keep the current webhook or send `null` to clear it. Responses return the URL as `alert_webhook_url` and never return the secret. |
| `owner`                         | `org` (default), `cluster`                     | See [Scopes](#scopes).                                                                                                                                                                                                                                                |

Each response also carries `id`, `scope`, `scope_id`, `scope_org_id`, and a live `spend` gauge: `{ "window", "cost_spent", "limit", "pct", "status" }`, where `status` is `ok` or `blocked`. For `user`, `team`, and `external_user` scopes, `scope_id` is `<org-id>:<id>`.

## Step 1 - Read the cluster currency

A cluster admin sets one currency for the whole cluster. Read it from the effective price card, which any bearer can read and which also lists the rates your spend is priced at. `currency` is `null` until the operator sets one.

<CodeGroup>
  ```ts MKA1 SDK theme={null}
  import { SDK } from '@meetkai/mka1';

  const mka1 = new SDK({
    bearerAuth: 'Bearer <mka1-api-key>',
  });

  const { currency } = await mka1.llm.models.listEffectivePrices({});
  console.log(currency); // e.g. "USD", or null until a cluster admin sets one
  ```

  ```csharp C# SDK theme={null}
  using MeetKai.MKA1;
  using MeetKai.MKA1.Types.Requests;

  var sdk = new SDK(bearerAuth: "Bearer <mka1-api-key>");

  var prices = await sdk.Llm.Models.ListEffectivePricesAsync();
  Console.WriteLine(prices.EffectivePricesResponse!.Currency); // e.g. "USD", or null until a cluster admin sets one
  ```

  ```python Python SDK theme={null}
  import os

  from meetkai_mka1 import SDK

  sdk = SDK(bearer_auth="Bearer <mka1-api-key>")

  prices = sdk.llm.models.list_effective_prices()
  print(prices.currency)  # e.g. "USD", or None until a cluster admin sets one
  ```

  ```bash bash theme={null}
  curl https://apigw.mka1.com/api/v1/llm/models/pricing/effective \
    --header 'Authorization: Bearer <mka1-api-key>'
  ```
</CodeGroup>

`budgets.getCurrency()` reads the budgeting service's copy of this code, but it is cluster-admin only; see [For cluster admins](#for-cluster-admins).

## Step 2 - Cap an end user

This example caps one end user at 5 per day, warns at 80%, and blocks at 100%. The `id` is the value you send in `X-On-Behalf-Of` on that user's requests. Setting a budget is an upsert: calling it again for the same `id` and `owner` replaces the budget and re-evaluates it against live spend.

<CodeGroup>
  ```ts MKA1 SDK theme={null}
  const budget = await mka1.budgets.setExternalUser({
    id: 'customer-4821', // the X-On-Behalf-Of id you send on their requests
    requestBody: {
      period: 'daily',
      limit: 5, // in the cluster currency
      thresholds: [
        { pct: 80, action: 'alert' },
        { pct: 100, action: 'block' },
      ],
      webhookUrl: 'https://your-app.example.com/hooks/budget',
      webhookSecret: process.env.BUDGET_WEBHOOK_SECRET,
    },
  });
  console.log(budget.id, budget.spend.status); // "ok"
  ```

  ```csharp C# SDK theme={null}
  var budget = await sdk.Budgets.SetExternalUserAsync(
      id: "customer-4821", // the X-On-Behalf-Of id you send on their requests
      body: new SetExternalUserBudgetRequestBody()
      {
          Period = SetExternalUserBudgetPeriodRequest.Daily,
          Limit = 5, // in the cluster currency
          Thresholds = new List<SetExternalUserBudgetThresholdRequest>()
          {
              new SetExternalUserBudgetThresholdRequest() { Pct = 80, Action = SetExternalUserBudgetActionRequest.Alert },
              new SetExternalUserBudgetThresholdRequest() { Pct = 100, Action = SetExternalUserBudgetActionRequest.Block },
          },
          WebhookUrl = "https://your-app.example.com/hooks/budget",
          WebhookSecret = Environment.GetEnvironmentVariable("BUDGET_WEBHOOK_SECRET"),
      }
  );
  Console.WriteLine($"{budget.Object!.Id} {budget.Object!.Spend.Status}"); // "ok"
  ```

  ```python Python SDK theme={null}
  budget = sdk.budgets.set_external_user(
      id="customer-4821",  # the X-On-Behalf-Of id you send on their requests
      period="daily",
      limit=5,  # in the cluster currency
      thresholds=[
          {"pct": 80, "action": "alert"},
          {"pct": 100, "action": "block"},
      ],
      webhook_url="https://your-app.example.com/hooks/budget",
      webhook_secret=os.environ["BUDGET_WEBHOOK_SECRET"],
  )
  print(budget.id, budget.spend.status)  # "ok"
  ```

  ```bash bash theme={null}
  curl https://apigw.mka1.com/api/v1/budgeting/budgets/external-user/customer-4821 \
    --request PUT \
    --header 'Content-Type: application/json' \
    --header 'Authorization: Bearer <mka1-api-key>' \
    --data '{
      "period": "daily",
      "limit": 5,
      "thresholds": [
        { "pct": 80, "action": "alert" },
        { "pct": 100, "action": "block" }
      ],
      "webhook_url": "https://your-app.example.com/hooks/budget",
      "webhook_secret": "<webhook-secret>"
    }'
  ```
</CodeGroup>

The response is the budget with its live gauge:

```json theme={null}
{
  "id": "<budget-id>",
  "scope": "external_user",
  "scope_id": "<org-id>:customer-4821",
  "scope_org_id": "<org-id>",
  "owner": "org",
  "period": "daily",
  "limit": 5,
  "thresholds": [
    { "pct": 80, "action": "alert" },
    { "pct": 100, "action": "block" }
  ],
  "alert_webhook_url": "https://your-app.example.com/hooks/budget",
  "spend": {
    "window": "2026-08-26",
    "cost_spent": 0,
    "limit": 5,
    "pct": 0,
    "status": "ok"
  }
}
```

Members, teams, and API keys work the same way through `setUser`, `setTeam`, and `setApiKey({ apiKeyId, requestBody })`; the [platform overview](/docs/platform-getting-started#budgets) shows a key budget end to end. The organization's own budget takes no `id` at all; see [Step 6](#step-6---cap-the-whole-organization).

## Step 3 - Read a budget and its live spend

A scope can carry a self-budget and an operator ceiling, so reads return a list.

<CodeGroup>
  ```ts MKA1 SDK theme={null}
  const { data } = await mka1.budgets.getExternalUser({ id: 'customer-4821' });
  for (const budget of data) {
    console.log(budget.owner, budget.period, budget.spend.costSpent, budget.spend.limit, budget.spend.status);
  }
  ```

  ```csharp C# SDK theme={null}
  var budgets = await sdk.Budgets.GetExternalUserAsync(id: "customer-4821");
  foreach (var budget in budgets.Object!.Data)
  {
      Console.WriteLine($"{budget.Owner} {budget.Period}: {budget.Spend.CostSpent} of {budget.Spend.Limit} ({budget.Spend.Status})");
  }
  ```

  ```python Python SDK theme={null}
  budgets = sdk.budgets.get_external_user(id="customer-4821")
  for budget in budgets.data:
      print(budget.owner, budget.period, budget.spend.cost_spent, budget.spend.limit, budget.spend.status)
  ```

  ```bash bash theme={null}
  curl https://apigw.mka1.com/api/v1/budgeting/budgets/external-user/customer-4821 \
    --header 'Authorization: Bearer <mka1-api-key>'
  ```
</CodeGroup>

`spend` is the live gauge for the current window. `status` flips to `blocked` once spend crosses a block threshold and back to `ok` when the window resets.

## Step 4 - Review threshold events

Each time spend crosses a threshold, the platform records an event with the spend and limit at that moment. Events for a default cap name the member, team, or end user they fired for in `target_id`.

<CodeGroup>
  ```ts MKA1 SDK theme={null}
  const events = await mka1.budgets.externalUserEvents({ id: 'customer-4821' });
  for (const event of events.data) {
    console.log(event.type, event.pct, event.periodKey, event.spendSnapshot, event.limitSnapshot, event.createdAt);
  }
  ```

  ```csharp C# SDK theme={null}
  var events = await sdk.Budgets.ExternalUserEventsAsync(id: "customer-4821");
  foreach (var budgetEvent in events.Object!.Data)
  {
      Console.WriteLine($"{budgetEvent.Type} {budgetEvent.Pct}% in {budgetEvent.PeriodKey}: {budgetEvent.SpendSnapshot} of {budgetEvent.LimitSnapshot} at {budgetEvent.CreatedAt}");
  }
  ```

  ```python Python SDK theme={null}
  events = sdk.budgets.external_user_events(id="customer-4821")
  for event in events.data:
      print(event.type, event.pct, event.period_key, event.spend_snapshot, event.limit_snapshot, event.created_at)
  ```

  ```bash bash theme={null}
  curl https://apigw.mka1.com/api/v1/budgeting/budgets/external-user/customer-4821/events \
    --header 'Authorization: Bearer <mka1-api-key>'
  ```
</CodeGroup>

An event has `id`, `budget_id`, `type` (`threshold_alert` or `blocked`), `pct` (`null` when the event was not tied to one threshold), `period_key`, `target_id`, `spend_snapshot`, `limit_snapshot`, and `created_at`.

## Step 5 - Set a default cap for every end user

A default cap applies to each end user individually, not to all of them together. Set it once and every end user without an explicit budget of the same owner and period is capped against their own spend. The same operations exist for members (`setUserDefault`) and teams (`setTeamDefault`).

<CodeGroup>
  ```ts MKA1 SDK theme={null}
  await mka1.budgets.setExternalUserDefault({
    requestBody: {
      period: 'monthly',
      limit: 20,
      thresholds: [{ pct: 100, action: 'block' }],
    },
  });

  // The default caps in force, and the end users they have fired for
  const defaults = await mka1.budgets.getExternalUserDefault({});
  const fired = await mka1.budgets.externalUserDefaultEvents({});
  console.log(defaults.data.length, fired.data.map((e) => e.targetId));
  ```

  ```csharp C# SDK theme={null}
  await sdk.Budgets.SetExternalUserDefaultAsync(new SetExternalUserDefaultBudgetRequestBody()
  {
      Period = SetExternalUserDefaultBudgetPeriodRequest.Monthly,
      Limit = 20,
      Thresholds = new List<SetExternalUserDefaultBudgetThresholdRequest>()
      {
          new SetExternalUserDefaultBudgetThresholdRequest() { Pct = 100, Action = SetExternalUserDefaultBudgetActionRequest.Block },
      },
  });

  // The default caps in force, and the end users they have fired for
  var defaults = await sdk.Budgets.GetExternalUserDefaultAsync();
  var fired = await sdk.Budgets.ExternalUserDefaultEventsAsync();
  Console.WriteLine($"{defaults.Object!.Data.Count} default cap(s), {fired.Object!.Data.Count} event(s)");
  ```

  ```python Python SDK theme={null}
  sdk.budgets.set_external_user_default(
      period="monthly",
      limit=20,
      thresholds=[{"pct": 100, "action": "block"}],
  )

  # The default caps in force, and the end users they have fired for
  defaults = sdk.budgets.get_external_user_default()
  fired = sdk.budgets.external_user_default_events()
  print(len(defaults.data), [event.target_id for event in fired.data])
  ```

  ```bash bash theme={null}
  curl https://apigw.mka1.com/api/v1/budgeting/budgets/external-user-default \
    --request PUT \
    --header 'Content-Type: application/json' \
    --header 'Authorization: Bearer <mka1-api-key>' \
    --data '{
      "period": "monthly",
      "limit": 20,
      "thresholds": [{ "pct": 100, "action": "block" }]
    }'

  curl https://apigw.mka1.com/api/v1/budgeting/budgets/external-user-default/events \
    --header 'Authorization: Bearer <mka1-api-key>'
  ```
</CodeGroup>

To give one end user a different cap, set an explicit budget for them with the same owner and period. To remove the default, call `deleteExternalUserDefault({ owner: 'org' })`; only explicit budgets remain.

## Step 6 - Cap the whole organization

An organization budget counts everything the organization spends, across every key, member, team, and end user. It takes no `id`: the target is the caller's own organization. Cluster admins can also cap another organization, or put a ceiling on it; see [For cluster admins](#for-cluster-admins).

<CodeGroup>
  ```ts MKA1 SDK theme={null}
  await mka1.budgets.setOrg({
    requestBody: {
      period: 'monthly',
      limit: 2500,
      thresholds: [
        { pct: 50, action: 'alert' },
        { pct: 90, action: 'alert' },
        { pct: 100, action: 'block' },
      ],
      webhookUrl: 'https://your-app.example.com/hooks/budget',
      webhookSecret: process.env.BUDGET_WEBHOOK_SECRET,
    },
  });

  const { data } = await mka1.budgets.getOrg({});
  console.log(data.map((b) => [b.owner, b.spend.pct, b.spend.status]));
  ```

  ```csharp C# SDK theme={null}
  await sdk.Budgets.SetOrgAsync(new SetOrgBudgetRequestBody()
  {
      Period = SetOrgBudgetPeriodRequest.Monthly,
      Limit = 2500,
      Thresholds = new List<SetOrgBudgetThresholdRequest>()
      {
          new SetOrgBudgetThresholdRequest() { Pct = 50, Action = SetOrgBudgetActionRequest.Alert },
          new SetOrgBudgetThresholdRequest() { Pct = 90, Action = SetOrgBudgetActionRequest.Alert },
          new SetOrgBudgetThresholdRequest() { Pct = 100, Action = SetOrgBudgetActionRequest.Block },
      },
      WebhookUrl = "https://your-app.example.com/hooks/budget",
      WebhookSecret = Environment.GetEnvironmentVariable("BUDGET_WEBHOOK_SECRET"),
  });

  var orgBudgets = await sdk.Budgets.GetOrgAsync();
  foreach (var budget in orgBudgets.Object!.Data)
  {
      Console.WriteLine($"{budget.Owner}: {budget.Spend.Pct}% ({budget.Spend.Status})");
  }
  ```

  ```python Python SDK theme={null}
  sdk.budgets.set_org(
      period="monthly",
      limit=2500,
      thresholds=[
          {"pct": 50, "action": "alert"},
          {"pct": 90, "action": "alert"},
          {"pct": 100, "action": "block"},
      ],
      webhook_url="https://your-app.example.com/hooks/budget",
      webhook_secret=os.environ["BUDGET_WEBHOOK_SECRET"],
  )

  org_budgets = sdk.budgets.get_org()
  for budget in org_budgets.data:
      print(budget.owner, budget.spend.pct, budget.spend.status)
  ```

  ```bash bash theme={null}
  curl https://apigw.mka1.com/api/v1/budgeting/budgets/org \
    --request PUT \
    --header 'Content-Type: application/json' \
    --header 'Authorization: Bearer <mka1-api-key>' \
    --data '{
      "period": "monthly",
      "limit": 2500,
      "thresholds": [
        { "pct": 50, "action": "alert" },
        { "pct": 90, "action": "alert" },
        { "pct": 100, "action": "block" }
      ],
      "webhook_url": "https://your-app.example.com/hooks/budget",
      "webhook_secret": "<webhook-secret>"
    }'
  ```
</CodeGroup>

An organization past a block threshold is blocked everywhere: the LLM gateway returns `403`, and Compute terminates a running [service or job](/docs/compute-deployment) with no grace period.

## Step 7 - Delete a budget

Deleting takes the same `id` as setting, plus the `owner` of the budget to remove, because a scope can carry one of each. Enforcement and alerts stop at once. For a member, team, or end user, the organization's default cap of the same owner and period applies to them again.

<CodeGroup>
  ```ts MKA1 SDK theme={null}
  const result = await mka1.budgets.deleteExternalUser({ id: 'customer-4821', owner: 'org' });
  console.log(result.deleted); // true
  ```

  ```csharp C# SDK theme={null}
  var result = await sdk.Budgets.DeleteExternalUserAsync(
      id: "customer-4821",
      owner: DeleteExternalUserBudgetQueryParamOwner.Org
  );
  Console.WriteLine(result.Object!.Deleted); // True
  ```

  ```python Python SDK theme={null}
  result = sdk.budgets.delete_external_user(id="customer-4821", owner="org")
  print(result.deleted)  # True
  ```

  ```bash bash theme={null}
  curl "https://apigw.mka1.com/api/v1/budgeting/budgets/external-user/customer-4821?owner=org" \
    --request DELETE \
    --header 'Authorization: Bearer <mka1-api-key>'
  ```
</CodeGroup>

## List every budget

One call returns everything you can see. A cluster admin sees every budget on the cluster. An organization admin sees the organization's budgets, its default caps, and the budgets on its API keys. Default caps come back without a `spend` gauge.

<CodeGroup>
  ```ts MKA1 SDK theme={null}
  const { data } = await mka1.budgets.list();
  for (const budget of data) {
    const gauge = 'spend' in budget ? `${budget.spend.pct}% (${budget.spend.status})` : 'default cap';
    console.log(budget.scope, budget.scopeId, budget.owner, budget.period, budget.limit, gauge);
  }
  ```

  ```csharp C# SDK theme={null}
  var all = await sdk.Budgets.ListAsync();
  foreach (var entry in all.Object!.Data)
  {
      if (entry.ListBudgetsData1 is { } budget)
      {
          Console.WriteLine($"{budget.Scope} {budget.ScopeId} {budget.Owner} {budget.Period} {budget.Limit}: {budget.Spend.Pct}% ({budget.Spend.Status})");
      }
      else if (entry.ListBudgetsData2 is { } rule)
      {
          Console.WriteLine($"{rule.Scope} {rule.ScopeId} {rule.Owner} {rule.Period} {rule.Limit}: default cap");
      }
  }
  ```

  ```python Python SDK theme={null}
  all_budgets = sdk.budgets.list()
  for budget in all_budgets.data:
      spend = getattr(budget, "spend", None)
      gauge = f"{spend.pct}% ({spend.status})" if spend else "default cap"
      print(budget.scope, budget.scope_id, budget.owner, budget.period, budget.limit, gauge)
  ```

  ```bash bash theme={null}
  curl https://apigw.mka1.com/api/v1/budgeting/budgets \
    --header 'Authorization: Bearer <mka1-api-key>'
  ```
</CodeGroup>

## How enforcement works

The gateway asks the budgeting service for a verdict before it dispatches any billable request, and sends the full identity of the caller: organization, API key, member, team, and the `X-On-Behalf-Of` end user. The budgeting service checks every budget that applies to that identity, from the organization's budget down to the end user's own cap. The gateway does not gate control-plane routes that cost nothing.

On `/responses`, the check runs after the model resolves, so an unknown or inactive model still returns `404` before a budget block returns `403`.

### What a blocked request returns

| Field           | Value                                                                                                   |
| --------------- | ------------------------------------------------------------------------------------------------------- |
| HTTP status     | `403 Forbidden`                                                                                         |
| Content-Type    | `application/json`                                                                                      |
| Body            | `{ "error": { "message", "code": "budget_exceeded", "scope", "owner", "budget_id", "period", "pct" } }` |
| Tokens consumed | 0. The request never reaches the model.                                                                 |

```json theme={null}
{
  "error": {
    "message": "Request blocked: organization budget exceeded",
    "code": "budget_exceeded",
    "scope": "org",
    "owner": "org",
    "budget_id": "<budget-id>",
    "period": "monthly",
    "pct": 120
  }
}
```

`scope` tells you which budget blocked the request: `org`, `apikey`, `user`, `team`, `external_user`, or one of the `*_default` rules. When a `*_default` rule blocks, the response never names the member, team, or end user it landed on; read that rule's events endpoint for it. Some endpoints add `"type": "permission_error"` to the same envelope, so match on `code`, not `type`.

The MKA1 SDK raises `APIError` (`statusCode === 403`) in TypeScript, `APIException` in C#, and `SDKDefaultError` (`status_code == 403`) in Python. Unlike a `429`, retrying does not help: the request stays blocked until the period resets or an admin raises or deletes the budget.

<CodeGroup>
  ```ts MKA1 SDK theme={null}
  import { APIError } from '@meetkai/mka1/models/errors';

  try {
    await mka1.llm.responses.create({
      xOnBehalfOf: 'customer-4821',
      responsesCreateRequest: { model: 'auto', input: 'Hello' },
    });
  } catch (err) {
    if (err instanceof APIError && err.statusCode === 403) {
      const { error } = JSON.parse(err.body);
      if (error.code === 'budget_exceeded') {
        console.log(`${error.scope} budget at ${error.pct}% of its ${error.period} limit`);
      }
    }
  }
  ```

  ```python Python SDK theme={null}
  import json

  from meetkai_mka1.errors import SDKDefaultError

  try:
      sdk.llm.responses.create(
          x_on_behalf_of="customer-4821",
          model="auto",
          input="Hello",
      )
  except SDKDefaultError as e:
      if e.status_code != 403:
          raise
      error = json.loads(e.body)["error"]
      if error["code"] == "budget_exceeded":
          print(f"{error['scope']} budget at {error['pct']}% of its {error['period']} limit")
  ```

  ```bash bash theme={null}
  curl -s -w "\nHTTP %{http_code}" https://apigw.mka1.com/api/v1/llm/responses \
    --request POST \
    --header 'Content-Type: application/json' \
    --header 'Authorization: Bearer <mka1-api-key>' \
    --header 'X-On-Behalf-Of: customer-4821' \
    --data '{ "model": "auto", "input": "Hello", "stream": false }'
  ```
</CodeGroup>

In C#, catch `APIException` and read its `Body` the same way; [rate limiting](/docs/rate-limiting) shows that shape.

### Best-effort, fail-open

The gateway gives the budget check 1.5 seconds. If the budgeting service is down, slow, or returns something the gateway does not recognize, the request goes through and the gateway logs `budget_enforcement_fail_open`.

Two more things let a client at its limit overshoot by a few requests. The gateway caches each identity's verdict for 3 seconds, and it reports spend to the budgeting service in batches, so the counter lags the requests it counts by a fraction of a second, longer if a push fails and is retried. Leave headroom below the amount you can actually spend.

### How spend is attributed

The platform prices every billable unit at the rate in effect when it ran, and a later price change does not reprice it. Each unit reaches the budgeting service tagged with the organization, API key, member, team, end user, model, and task type, which is what `group_by` below can split on. Spend windows reset on UTC calendar boundaries; the gauge names them as `2026-07-07`, `2026-W28`, or `2026-07` for daily, weekly, and monthly budgets.

A unit with no cost counts against nothing: a model with no price rates to zero, and a unit the gateway could not rate at all, as during a pricing outage, gets a null cost. If a budget's gauge stays at `0` while requests flow, check the model's price under **Admin → Pricing** before you check the budget.

To see where the money went, `usage.costs` sums cost over a time range across every billable service. Its `group_by` is a comma-separated string of any of `service`, `task_type`, `model`, `api_key_id`, `team_id`, `user_id`, `external_user_id`, and `org_id`, in curl and in every SDK:

```bash theme={null}
curl "https://apigw.mka1.com/api/v1/budgeting/usage/costs?start_time=1787702400&end_time=1787788800&group_by=external_user_id" \
  --header 'Authorization: Bearer <mka1-api-key>'
```

The LLM gateway's own report, `llm.usage.costs`, covers LLM spend only and types `groupBy` as an enum of `model`, `api_key_id`, `team_id`, `external_user_id`, and `org_id`.

## In the console

**Admin → Budgets** manages the same budgets with three tabs:

* **Organization** (or **Organizations** in cluster scope): the organization's self-budget and operator ceiling, each with its live gauge.
* **Members**: explicit member budgets plus the organization's per-member default cap. The default cap row has no gauge; its **History** action lists the members it has fired for.
* **API keys**: cluster scope only; see [For cluster admins](#for-cluster-admins).

Every row shows live spend against its limit and a status badge of `ok` or `blocked`. The **History** action on a row expands its thresholds and the period's alert and block events. Team and end-user budgets, and their defaults, are API-only.

## Reference

| Scope                     | Set                                       | Read                         | Delete                                 | Events                          |
| ------------------------- | ----------------------------------------- | ---------------------------- | -------------------------------------- | ------------------------------- |
| Organization              | `setOrg({ requestBody })`                 | `getOrg({})`                 | `deleteOrg({ owner })`                 | `orgEvents({})`                 |
| API key                   | `setApiKey({ apiKeyId, requestBody })`    | `getApiKey({ apiKeyId })`    | `deleteApiKey({ apiKeyId, owner })`    | `apiKeyEvents({ apiKeyId })`    |
| Member                    | `setUser({ id, requestBody })`            | `getUser({ id })`            | `deleteUser({ id, owner })`            | `userEvents({ id })`            |
| Team                      | `setTeam({ id, requestBody })`            | `getTeam({ id })`            | `deleteTeam({ id, owner })`            | `teamEvents({ id })`            |
| End user                  | `setExternalUser({ id, requestBody })`    | `getExternalUser({ id })`    | `deleteExternalUser({ id, owner })`    | `externalUserEvents({ id })`    |
| Member default            | `setUserDefault({ requestBody })`         | `getUserDefault({})`         | `deleteUserDefault({ owner })`         | `userDefaultEvents({})`         |
| Team default              | `setTeamDefault({ requestBody })`         | `getTeamDefault({})`         | `deleteTeamDefault({ owner })`         | `teamDefaultEvents({})`         |
| End-user default          | `setExternalUserDefault({ requestBody })` | `getExternalUserDefault({})` | `deleteExternalUserDefault({ owner })` | `externalUserDefaultEvents({})` |
| All                       |                                           | `list()`                     |                                        |                                 |
| Currency (cluster admins) | `setCurrency({ currency })`               | `getCurrency()`              |                                        |                                 |

Python uses the same names in `snake_case` with flat keyword arguments, for example `sdk.budgets.set_user(id=..., period=..., limit=..., thresholds=[...], owner="org")`. C# uses `PascalCase` with an `Async` suffix and a `body` parameter, for example `sdk.Budgets.SetUserAsync(id: ..., body: new SetUserBudgetRequestBody { ... })`. Request types live in `MeetKai.MKA1.Types.Requests`. The optional `orgId` on every organization-scoped operation is for cluster admins; see the next section.

## For cluster admins

<Note>
  Everything in this section needs a cluster admin bearer. Organization admins get `403` here.
</Note>

* **Operator ceilings.** Pass `owner: 'cluster'` to any set operation to put a ceiling on that scope. Organization admins can read it but not change it, and it applies alongside their own `org` budget; the most restrictive wins. Delete it with the same `owner: 'cluster'`.
* **Other organizations.** Every organization-scoped operation takes an optional `orgId` (`org_id` in Python and curl) that targets another organization instead of your own. `list()` returns every budget on the cluster.
* **API-key budgets in the console.** The **API keys** tab on **Admin → Budgets** appears only in cluster scope, which needs the cluster organization to be the active one; a cluster-admin role held from an organization session is not enough. Budgets made there always carry `owner: "cluster"`.
* **Display currency.** `budgets.getCurrency()` and `budgets.setCurrency()` read and set the ISO 4217 code the budgeting service shows on budgets and cost reports. It is a display copy. Spend is rated in the price book's currency, which `setClusterCurrency` sets (see [Manage models](/docs/models#for-cluster-admins)); the currency control under **Admin → Pricing** writes both. Over the API, set both, once, before the first budget, because neither converts any amount. If the console shows the two apart after a first save, save again.

<CodeGroup>
  ```ts MKA1 SDK theme={null}
  await mka1.budgets.setCurrency({ currency: 'EUR' });
  const { currency } = await mka1.budgets.getCurrency();
  console.log(currency); // "EUR"
  ```

  ```csharp C# SDK theme={null}
  await sdk.Budgets.SetCurrencyAsync(new SetCurrencyRequest() { Currency = "EUR" });
  var currency = await sdk.Budgets.GetCurrencyAsync();
  Console.WriteLine(currency.Object!.Currency); // "EUR"
  ```

  ```python Python SDK theme={null}
  sdk.budgets.set_currency(currency="EUR")
  currency = sdk.budgets.get_currency()
  print(currency.currency)  # "EUR"
  ```

  ```bash bash theme={null}
  curl https://apigw.mka1.com/api/v1/budgeting/settings/currency \
    --request PUT \
    --header 'Content-Type: application/json' \
    --header 'Authorization: Bearer <mka1-api-key>' \
    --data '{ "currency": "EUR" }'

  curl https://apigw.mka1.com/api/v1/budgeting/settings/currency \
    --header 'Authorization: Bearer <mka1-api-key>'
  ```
</CodeGroup>

## Troubleshooting

| Symptom                                                       | Cause                                                                                                                                                                        | Fix                                                                                                                |
| ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| A normal request returns `403` with `code: "budget_exceeded"` | Spend crossed a block threshold on the budget named by `scope` and `owner`.                                                                                                  | Wait for the period to reset, or find the budget with `list()` and raise or delete it.                             |
| A budgets call itself returns `403`                           | The key lacks `read:budgets` or `write:budgets`, the caller is not an organization admin, or the call needs a cluster admin (see [For cluster admins](#for-cluster-admins)). | Grant the scopes in the key form; use `owner: "org"` and no `orgId` unless you are a cluster admin.                |
| The gauge stays at `0` while requests flow                    | The model has no price, so it rates to zero, or the gateway could not rate the requests.                                                                                     | Set a price under **Admin → Pricing**.                                                                             |
| A blocked scope still gets through occasionally               | The verdict cache, the batched spend push, or a fail-open while the budgeting service was unreachable.                                                                       | Expected. Set the limit below the amount you can actually spend.                                                   |
| An end user is not capped by the default                      | Requests for that user are missing `X-On-Behalf-Of`, or an explicit budget of the same owner and period overrides the default.                                               | Send the header, and read `getExternalUser({ id })` to see what applies.                                           |
| An end user is neither capped nor counted                     | The `X-On-Behalf-Of` ID is longer than 255 characters, so the gateway drops it from the budgeting identity.                                                                  | Keep IDs at 255 characters or fewer.                                                                               |
| The webhook never fires                                       | Alert thresholds fire once per period, only when spend crosses them, and only for `alert` actions.                                                                           | Check `externalUserEvents` (or the scope's equivalent) for the `threshold_alert` event, then your endpoint's logs. |

## API reference

For the full request and response schemas, open the Budgets, Settings, and Usage groups in the [API Reference](/api-reference/budgets/list-budgets).

## See also

* [Rate limiting](/docs/rate-limiting) - request-count limits, which return `429` instead of `403`.
* [Authentication](/docs/authentication) - API keys, scopes, and the `X-On-Behalf-Of` pattern that identifies end users.
* [Platform overview: Budgets](/docs/platform-getting-started#budgets) - the console walkthrough and an API-key budget example.
