Usage and budgets
For organization administrators. You need the rights named for each task. Only a
platform admin can change settings under /v1/platform. Start with the
administrator manual.
OpenBot records each call to a model. You can see how many tokens each member, Bot, and user token used, and what they cost. A token is a small piece of text that a model reads or writes. You can also set budgets that stop model calls when too much money is spent. A platform can give organizations credits to pay for the models that the platform shares.
| Word | Meaning |
|---|---|
| Usage event | One model call: its tokens, its cost, and who made it. |
| Price | What one million tokens of a model cost at one provider. |
| Budget | A spending limit for the organization, a member, a Bot, or a user token, for one day or one month. |
| Credits | Money that the platform gives an organization to pay for the models of the platform. |
| Micro-USD | All money in the API is in micro-USD. One US dollar is 1,000,000 micro-USD. |
| Platform key | A shared model provider. The platform pays for it. |
| Own key | A model provider of the organization. The organization pays the model company itself. |
ChatGPT providers use the same prices, budgets, and credits as other model providers. Organization providers do not take platform credits. Platform providers use the configured price and credits rules. ChatGPT account limits also apply.
What OpenBot records
Section titled “What OpenBot records”OpenBot records a model call before it goes to the provider. Each usage event
has a source:
source |
What made the call |
|---|---|
turn |
A model step of a Turn in a Chat. |
compaction |
A summary of old messages, so the conversation fits the model. |
judge |
The model that judges Rules. |
bot_memory |
The memory Bot: what it learns, adds and answers. |
other |
Any other call. |
A usage event also has the Chat (chatId), the Turn, the Bot, the member and
the user token, when the call has them. In a Channel,
the member is the person who wrote the message.
Each call has a stable id. A durable step also has an executionId. Its
status is pending while it runs, confirmed when the provider gives usage,
or unknown when usage is missing. Unknown token counts and cost are null,
not zero. The same confirmed receipt never charges the call twice.
Before sending, OpenBot holds an estimate in reservedMicros. It uses about
four text characters per input token and 1,024 output tokens. Other input parts
get a rough size. The actual cost can be higher. The saved estimate stays in
estimatedMicros after the call ends. This field is null when an old record
no longer has its estimate.
Calls with pending or unknown usage keep their holds. Ending or stopping an
execution, or changing its owner, does not release an unknown hold. Only a
confirmed receipt settles the actual cost and releases the hold. The receipt may
confirm zero cost; missing usage cannot. A late receipt settles the call once.
The summary counts confirmed calls, pendingCalls, and unknownCalls apart.
A host action with no model call has no model usage.
Model billing and token rate errors keep their full cause in a sealed error record. Public errors and ordinary logs carry a safe category and an error ID. If a confirmed usage write fails, the sealed record also keeps the call ID and token counts for repair. It does not turn unknown usage into a zero charge.
An invalid page cursor returns 400 UsageQueryInvalid. A price needs at least
one tier, so every token count has a price.
Prices
Section titled “Prices”A price belongs to one model at one provider, because the same model can cost a different amount at another provider. Many models cost more when a request is long, and some providers cost less at night. So a price has tiers by input size and periods by time of day.
Each tier has rates. Each rate is the cost of one million tokens, in micro-USD:
| Field | Which tokens |
|---|---|
inputUncached |
Input tokens that the provider did not read from its cache. |
inputCacheRead |
Input tokens that the provider read from its cache. |
inputCacheWrite |
Input tokens that the provider wrote to its cache. |
output |
Output text and tool calls. |
outputReasoning |
Reasoning tokens. Send null to use the rate of output. |
An administrator of the organization sets the prices of the organization’s own providers. These prices are an estimate of what the model company charges. A platform admin sets the prices of the shared providers. For this example, input costs 2.50 US dollars and output costs 10 US dollars for one million tokens, for all input sizes and at all times:
curl -X PUT -H "Authorization: Bearer $OPENBOT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4.1","tiers":[{"upToInputTokens":null,"inputUncached":2500000,"inputCacheRead":625000,"inputCacheWrite":0,"output":10000000,"outputReasoning":null}]}' \ https://api.example.com/v1/organization/model_providers/prov_4c1e/prices| Route | What it does |
|---|---|
GET /v1/organization/model_providers/:providerId/prices |
Lists the current price of each model of your own provider. |
PUT /v1/organization/model_providers/:providerId/prices |
Sets a new price for one model of your own provider. |
GET /v1/platform/model_providers/:providerId/prices |
Lists the current price of each model of a shared provider. |
PUT /v1/platform/model_providers/:providerId/prices |
Sets a new price for one model of a shared provider. |
A provider can only price the models that it links. Another model gives 400
with ModelProviderInvalid. The model catalog shows
the current price of each model.
A new price applies to calls that start from now on. Each reservation reads the price from the database and keeps its own copy. A price change on another node, or removal of the provider, cannot change the price of that call. The saved usage keeps its cost. OpenBot rounds the cost of each call up to a whole micro-USD.
A model without a price costs 0. Its calls are still recorded, but they do not count toward budgets and do not take credits.
Tiers by input size
Section titled “Tiers by input size”tiers is a list, from small to large. Each tier has upToInputTokens: the
most input tokens that a call in this tier can have. The input tokens of a call
are all its input tokens, from the cache or not. The last tier has
upToInputTokens: null, because it has no upper limit. You can have up to 20
tiers.
A call uses the first tier that its input fits. All tokens of the call then cost the rates of that tier, not only the tokens over the limit. Most providers bill long requests in this way.
For example, a model costs less up to 32,000 input tokens, more up to 128,000, and the most above that:
{ "model": "qwen-plus", "tiers": [ { "upToInputTokens": 32000, "inputUncached": 800000, "inputCacheRead": 160000, "inputCacheWrite": 0, "output": 2000000, "outputReasoning": 8000000 }, { "upToInputTokens": 128000, "inputUncached": 2400000, "inputCacheRead": 480000, "inputCacheWrite": 0, "output": 6000000, "outputReasoning": 24000000 }, { "upToInputTokens": null, "inputUncached": 4800000, "inputCacheRead": 960000, "inputCacheWrite": 0, "output": 12000000, "outputReasoning": 48000000 } ]}A call with 50,000 input tokens uses the second tier.
Periods by time of day
Section titled “Periods by time of day”periods gives other tiers for some times, such as a lower price at night. Each
period has a name, one or more windows, and its own tiers. timeZone says
in which time zone the windows are. It is an IANA name, such as UTC or
Asia/Shanghai. Leave it out for UTC.
| Window field | What it is |
|---|---|
start |
The start, as HH:MM. The window includes this minute. |
end |
The end, as HH:MM, up to 24:00. The window stops before this minute. |
days |
The days on which the window starts: mon, tue, wed, thu, fri, sat, sun. Leave it out for every day. |
When end is before start, the window goes past midnight. It belongs to the
day on which it starts. For example,
{"start":"22:00","end":"02:00","days":["fri"]} holds Friday 23:00 and Saturday
01:00, but not Friday 01:00.
OpenBot takes the time at which the call starts. It uses the first period with a
window that holds this time. When no period holds it, the call uses the tiers
of the price. In each case, the call then picks its tier by input size.
For example, a provider costs half from 00:30 to 08:30 in Beijing time:
{ "model": "deepseek-chat", "timeZone": "Asia/Shanghai", "tiers": [ { "upToInputTokens": null, "inputUncached": 2000000, "inputCacheRead": 500000, "inputCacheWrite": 0, "output": 8000000, "outputReasoning": null } ], "periods": [ { "name": "off-peak", "windows": [{ "start": "00:30", "end": "08:30" }], "tiers": [ { "upToInputTokens": null, "inputUncached": 1000000, "inputCacheRead": 250000, "inputCacheWrite": 0, "output": 4000000, "outputReasoning": null } ] } ]}Leave periods out when the price is the same at all times.
See the usage
Section titled “See the usage”GET /v1/organization/usage adds up the tokens and the cost in a time range. It
needs read on Usage.
| Query | What it does |
|---|---|
from |
The start, as an ISO 8601 time. The default is the start of the current month in UTC. |
to |
The end, as an ISO 8601 time. The default is now. |
groupBy |
day (the default), model, provider, member, bot, token or source. |
A time without a zone, such as 2026-10-01T00:00:00, is in UTC.
curl -H "Authorization: Bearer $OPENBOT_TOKEN" \ "https://api.example.com/v1/organization/usage?groupBy=member"{ "from": "2026-10-01T00:00:00.000Z", "to": "2026-10-23T09:00:00.001Z", "groupBy": "member", "total": { "calls": 42, "inputUncached": 81200, "inputCacheRead": 20400, "inputCacheWrite": 0, "outputText": 9800, "outputReasoning": 1200, "costMicros": 325250 }, "items": [ { "key": "u_123", "calls": 42, "inputUncached": 81200, "inputCacheRead": 20400, "inputCacheWrite": 0, "outputText": 9800, "outputReasoning": 1200, "costMicros": 325250 } ]}key is the value of the group, such as a user ID or a day. It is null for
calls without that value, such as calls without a Bot.
GET /v1/organization/usage/events lists the single usage events, newest first.
It takes from, to, limit (1 to 200, the default is 50) and after. Send
the next value of a page as after to get the next page. Each event has
priced and charged. charged is true when the cost came from the credits.
Budgets
Section titled “Budgets”A budget checks model calls against a spending limit. You can set a budget on four scopes:
scope |
scopeId |
What counts |
|---|---|---|
organization |
Leave it out. It is the organization ID. | Every call of the organization. |
member |
The user ID of a member. | The calls that work for this member. |
bot |
The ID of a Bot. | The calls of this Bot. |
token |
The ID of a user token. | The calls of work that this token started. |
Each budget has a period: day or month. Both start at midnight UTC. Each
scope can have one budget for each period.
Each budget also has a mode:
- A
hardbudget stops a model call when its estimate does not fit beside confirmed cost for the period and all held amounts. The call fails, so the Turn fails with a model error. - A
softbudget never stops a call. It only showsexceeded: true.
A budget counts every priced call, also calls with the organization’s own key.
Before a call, OpenBot checks confirmed spend and open reservations together.
All nodes share this check. A call stops when its estimate does not fit. Holds
for pending and unknown calls still count after a new day or month starts.
Only a confirmed cost, including an explicit zero-cost receipt, settles the call
and releases the hold. The same receipt never adds a charge twice. Confirmed
costs count in the period when the receipt is recorded. The final cost can
exceed the estimate, so a budget is not a promise that the provider’s final
charge will stay below its limit.
An old unknown call with a saved estimatedMicros still counts as held money,
even if reservedMicros is 0. Missing old estimates stay null. If an old
unknown call has no estimate and may have a cost, OpenBot stops new calls under
the hard budgets that cover it until its cost is confirmed. For platform calls,
this also applies to the affected credit limit. A saved zero estimate does not
cause this missing-estimate block or add a hold. An old call known to be free
does not cause this block either. Other budget and credit limits still apply to
new calls.
curl -H "Authorization: Bearer $OPENBOT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"scope":"bot","scopeId":"bot_7d2c","period":"month","limitMicros":50000000,"mode":"hard"}' \ https://api.example.com/v1/organization/budgets{ "id": "budget_5c9e", "scope": "bot", "scopeId": "bot_7d2c", "period": "month", "limitMicros": 50000000, "mode": "hard", "periodStart": "2026-10-01T00:00:00.000Z", "spentMicros": 1250000, "exceeded": false, "version": 1, "createdAt": "2026-10-23T09:00:00.000Z", "updatedAt": "2026-10-23T09:00:00.000Z"}| Route | What it does |
|---|---|
GET /v1/organization/budgets |
Lists the budgets, with their spend now. |
POST /v1/organization/budgets |
Adds a budget. |
GET /v1/organization/budgets/:budgetId |
Reads one budget. |
PATCH /v1/organization/budgets/:budgetId |
Changes limitMicros or mode. Send the version as expectedVersion. |
DELETE /v1/organization/budgets/:budgetId |
Removes a budget. |
Reading budgets needs read on Budget. Changing them needs write on
Budget. Each change goes to the audit log.
Credits
Section titled “Credits”Credits let a platform sell or give the use of its shared models. They are off by default. Then an organization can use the shared models without limit, unless a budget stops it.
A platform admin turns credits on for one organization, and gives credits:
curl -X PATCH -H "Authorization: Bearer $OPENBOT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"enabled":true}' \ https://api.example.com/v1/platform/organizations/org_8f3a/credits
curl -H "Authorization: Bearer $OPENBOT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"amountMicros":20000000,"note":"Trial for October"}' \ https://api.example.com/v1/platform/organizations/org_8f3a/credits/grantsA negative amountMicros takes credits back. Each grant goes to the platform
audit log.
While credits are on:
- Each confirmed call to a model of the platform takes its cost from the balance.
- A call stops when no balance remains after all holds, or its estimate does not fit. Calls with the organization’s own key are not subject to platform credits.
- OpenBot checks the balance and held amounts before a call. A confirmed receipt takes the actual cost from the balance and releases its hold. A cost above the estimate can take the balance below 0.
- Calls with
pendingorunknownusage keep their holds. Unknown usage does not reduce the confirmed balance, but the held credits cannot be used again. A late confirmed receipt takes the actual cost once and releases the hold. This can take the balance below 0. - An old unknown platform call with no estimate and a possible cost stops new platform calls until its cost is confirmed. A saved zero estimate or an old call known to be free does not cause this missing-estimate block or add a hold. Other credit limits still apply to new calls.
Calls with the organization’s own key never take credits, also when credits are on.
GET /v1/organization/credits shows enabled, balanceMicros and the newest
50 grants. It needs read on Usage. A platform admin reads the same with
GET /v1/platform/organizations/:organizationId/credits.
OpenBot has no payments yet. A platform that sells credits records the payment in its own system, then gives the credits with the API.
When a call is stopped
Section titled “When a call is stopped”When a budget or the credits stop a call, the model call fails with an AiError
of the reason QuotaExhaustedError. The Turn ends with the status Failed and
the code model_error. The metadata of the error says why, in openbot.reason:
budget or credits. For a budget, openbot.budgetId names it.
When OpenBot cannot read the budgets, for example because the database is down, it stops the call too.
Rate limit
Section titled “Rate limit”A platform admin can limit how many API requests each caller sends in one
minute. Each user token counts on its own. A signed-in person without a token
counts as one caller. Every instance of OpenBot shares the count. Set
rateLimit in the platform settings:
curl -X PATCH -H "Authorization: Bearer $OPENBOT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"expectedVersion":4,"rateLimit":{"requestsPerMinute":600}}' \ https://api.example.com/v1/platform/settingsnull means no limit. This is the default. A caller over the limit gets 429
with RateLimited. retryAfterSeconds says when the next minute starts. The
limit covers the API of organizations. It does not cover /v1/platform,
sign-in, or webhooks.
Token rate
Section titled “Token rate”A token rate limits how fast the model work of a scope uses tokens. It counts input and output tokens together. When work goes over the rate, OpenBot slows it down. It never refuses the work.
Each scope has a token bucket. The bucket holds up to burstTokens and
fills again at tokensPerSecond. A full bucket lets a short burst of work
through at full speed.
| Scope | One bucket for |
|---|---|
organization |
All model calls of the organization. |
member |
The model calls that work for one member. |
bot |
The model calls of one Bot. |
token |
The model calls of work that one user token started. |
A model call takes from every bucket of its scopes:
- Before the call, it takes an input estimate from each shared bucket and waits for its turn. A call without streamed output also takes a rough output estimate. OpenBot checks the budgets and the credits after this wait.
- While the model streams, OpenBot reads the output from the provider into a buffer. It sends each piece on only when the buckets have room for it, about one token for every four characters. So the output comes at an even speed.
- When the call ends, confirmed counts replace the estimates. Extra tokens add debt, and unused estimates go back into the buckets. A stop while output waits still uses any counts that the provider has sent. Without confirmed counts, the rate keeps the estimate. This does not charge money.
The platform admin sets the highest rates in the
platform settings. An organization can set its
own rates in its settings. For each scope, the lower
of the two rates applies, so an organization can only make a rate lower. null
means no limit for that scope. The default is no limit.
curl -X PATCH -H "Authorization: Bearer $OPENBOT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"expectedVersion":5,"tokenRate":{"organization":{"tokensPerSecond":2000,"burstTokens":20000},"member":{"tokensPerSecond":500,"burstTokens":10000},"bot":null,"token":null}}' \ https://api.example.com/v1/platform/settingsAll instances share the same buckets in PostgreSQL. Taking tokens from all scopes is one transaction, and refill uses the database clock. Starting more instances does not raise a scope’s rate. Stopping a waiting call does not erase its shared debt. A database failure stops model admission until the rates can be checked again.
Errors
Section titled “Errors”| Status | Error | When |
|---|---|---|
400 |
UsageQueryInvalid |
from or to is not an ISO 8601 time, or from is not before to. |
400 |
BudgetInvalid |
The scope ID is missing, or no member or token of the organization has it. |
400 |
ModelProviderInvalid |
The provider does not link the model that you want to price. |
403 |
Forbidden |
The caller may not do this on Usage or Budget. |
404 |
BudgetNotFound |
No budget of the organization has this ID. |
409 |
BudgetConflict |
The scope already has a budget for this period, or the budget changed. |
429 |
RateLimited |
The caller sent too many requests in this minute. |
503 |
BillingUnavailable |
OpenBot cannot reach its database. |
See the full shapes in the Usage reference and the Budgets reference.