Usage
Read your account usage and cost from a script or an agent: totals per model, a series per day, or the log of single requests. These endpoints are specific to kenari and have no OpenAI or Anthropic equivalent. They return the same figures as the Usage page in the dashboard.
GET /v1/usageGET /v1/usage/dailyGET /v1/usage/logAll three need an API key in the Authorization header. A key without restrictions reads the usage of the whole account, with every API key on it. See Who can read what.
Request
Section titled “Request”All three endpoints take query parameters and no body. Every parameter is optional. An unknown parameter is ignored. If a parameter is repeated, the first value is used.
| Parameter | Type | Endpoints | Description |
|---|---|---|---|
start | integer | all | Start of the window, in Unix seconds, inclusive. |
end | integer | all | End of the window, in Unix seconds, inclusive. A value in the future is treated as now. |
range | string | all | A preset window that ends now: today (since 00:00 UTC), 24h, 7d or 30d. The default is 30d. |
model | string | all | Only this exact model id. |
api_key_id | string | all | Only requests made with this key id. See Who can read what for restricted keys. |
billing_kind | string | /v1/usage/log | Only this billing kind: payg, plan, coupon, token_alloc, free or byok. |
page | integer | /v1/usage/log | Page number, starting at 0. The default is 0. |
limit | integer | /v1/usage/log | Requests per page. The default is 50. A value outside 1 to 200 is moved to the nearest limit, not rejected. |
Window rules
Section titled “Window rules”All three endpoints resolve the window in the same way.
- With no
start,endorrange, the window is the last 30 days. startandendwin overrange. When either is present,rangeis ignored, even if its value is not valid.- With only
start, the window ends now. With onlyend, it starts 30 days beforeend. endis clamped to now.startandendmust not be negative, andstartmust not be afterend./v1/usageand/v1/usage/dailyaccept a window that touches at most 366 UTC days. A longer window is a400./v1/usage/loghas no such cap, but see the depth limit below.
The response always repeats the resolved start and end, so you can see the window that was used.
Billing kinds
Section titled “Billing kinds”billing_kind says what paid for a request.
| Value | Meaning |
|---|---|
payg | PAYG, paid from your balance. |
plan | Covered by your plan quota. |
coupon | Covered by a coupon. |
token_alloc | Covered by a token allocation. |
free | A free model. |
byok | Served by your own BYOK key. |
Every amount is an integer in micro-Rupiah, which is Rupiah times 1,000,000. Rp 1 is 1000000. See How billing works. Each row has two amounts.
cost_micro_idris what the requests took from your balance. Plan, coupon, free and BYOK usage is0here, because none of it is paid from balance.catalog_micro_idris what the same usage is worth at the list price of the model. It lets you see the value you got from a plan, a coupon, a free model or a BYOK key, where the charge can be lower than the list price or0.
Summary
Section titled “Summary”GET /v1/usage returns one row per model and billing kind, sorted by cost_micro_idr from high to low, plus a total row. Plan usage is reported under plan. A request with no recorded billing kind is reported as payg.
| Field | Type | Description |
|---|---|---|
object | string | Always usage.summary. |
start | integer | Start of the window, in Unix seconds. |
end | integer | End of the window, in Unix seconds. |
data | array | One row per model and billing kind. Empty when there is no usage. |
data[].model | string | The model id. |
data[].billing_kind | string | One of the billing kinds above. |
data[].requests | integer | Number of requests. |
data[].prompt_tokens | integer | Input tokens. |
data[].completion_tokens | integer | Output tokens. |
data[].cached_prompt_tokens | integer | Input tokens read from the cache. |
data[].cache_write_prompt_tokens | integer | Input tokens written to the cache. |
data[].cost_micro_idr | integer | Amount taken from your balance, in micro-Rupiah. |
data[].catalog_micro_idr | integer | Value at list price, in micro-Rupiah. |
total | object | The same seven counters, summed over all rows. |
{ "object": "usage.summary", "start": 1788300000, "end": 1790892000, "data": [ { "model": "deepseek-v4-flash", "billing_kind": "payg", "requests": 120, "prompt_tokens": 480000, "completion_tokens": 96000, "cached_prompt_tokens": 200000, "cache_write_prompt_tokens": 0, "cost_micro_idr": 15400000, "catalog_micro_idr": 15400000 }, { "model": "step-3-7-flash:free", "billing_kind": "free", "requests": 40, "prompt_tokens": 52000, "completion_tokens": 18000, "cached_prompt_tokens": 0, "cache_write_prompt_tokens": 0, "cost_micro_idr": 0, "catalog_micro_idr": 900000 } ], "total": { "requests": 160, "prompt_tokens": 532000, "completion_tokens": 114000, "cached_prompt_tokens": 200000, "cache_write_prompt_tokens": 0, "cost_micro_idr": 15400000, "catalog_micro_idr": 16300000 }}Daily series
Section titled “Daily series”GET /v1/usage/daily returns one row per UTC day in the window, oldest first. A day with no usage is a row with zeros, so the series has no gaps. The model and api_key_id filters apply.
| Field | Type | Description |
|---|---|---|
object | string | Always usage.daily. |
start | integer | Start of the window, in Unix seconds. |
end | integer | End of the window, in Unix seconds. |
data | array | One row per UTC day. |
data[].date | string | The UTC day, as YYYY-MM-DD. |
data[].requests | integer | Number of requests on that day. |
data[].cost_micro_idr | integer | Amount taken from your balance on that day, in micro-Rupiah. |
{ "object": "usage.daily", "start": 1790287200, "end": 1790892000, "data": [ { "date": "2026-09-24", "requests": 18, "cost_micro_idr": 2100000 }, { "date": "2026-09-25", "requests": 0, "cost_micro_idr": 0 }, { "date": "2026-09-26", "requests": 7, "cost_micro_idr": 950000 } ]}Request log
Section titled “Request log”GET /v1/usage/log returns single requests, newest first. It supports every filter, including billing_kind. Plan usage is reported as plan.
| Field | Type | Description |
|---|---|---|
object | string | Always list. |
start | integer | Start of the window, in Unix seconds. |
end | integer | End of the window, in Unix seconds. |
data | array | The requests on this page. |
data[].created_at | integer | When the request was made, in Unix seconds. |
data[].model | string | The model id. |
data[].vendor | string | The maker of the model. |
data[].api_key_id | string or null | The id of the key that made the request. null when there is none. |
data[].billing_kind | string | One of the billing kinds above. |
data[].prompt_tokens | integer | Input tokens. |
data[].completion_tokens | integer | Output tokens. |
data[].cached_prompt_tokens | integer | Input tokens read from the cache. |
data[].cache_write_prompt_tokens | integer | Input tokens written to the cache. |
data[].cost_micro_idr | integer | Amount taken from your balance, in micro-Rupiah. |
data[].catalog_micro_idr | integer | Value at list price, in micro-Rupiah. |
page | integer | The page number that was used. |
limit | integer | The page size that was used, after clamping. |
has_more | boolean | true when another page follows. |
total | integer | Number of requests that match the filters in the whole window, not only this page. |
{ "object": "list", "start": 1788300000, "end": 1790892000, "data": [ { "created_at": 1790891000, "model": "deepseek-v4-flash", "vendor": "DeepSeek", "api_key_id": "a1b2c3d4", "billing_kind": "payg", "prompt_tokens": 4200, "completion_tokens": 310, "cached_prompt_tokens": 2000, "cache_write_prompt_tokens": 0, "cost_micro_idr": 128000, "catalog_micro_idr": 128000 } ], "page": 0, "limit": 50, "has_more": true, "total": 120}page times limit must not be more than 10,000. A deeper page is a 400. To read further back, narrow the window with start and end and read it in slices.
Who can read what
Section titled “Who can read what”- A key without restrictions reads the usage of the whole account, and can narrow it with
api_key_id. - A key with a model scope, a spend cap or a token cap is restricted. It reads only the usage of its own key. Leave
api_key_idout, or set it to the key’s own id. Any other id is a400. - A shared API key cannot read usage at all. It gets
403andshared_key_not_allowed.
See Authentication and API keys for how to restrict a key.
Limits
Section titled “Limits”The three endpoints share one limit of 60 requests per minute for each account, whichever key makes them. Over the limit, the request fails with 429 and rate_limit_exceeded, and there is no Retry-After header. Wait a few seconds and retry. This limit is counted apart from the request limit of chat, so reading usage does not use up your chat requests. See Rate limits.
Examples
Section titled “Examples”curl "https://kenari.id/v1/usage?range=7d" \ -H "Authorization: Bearer $KENARI_API_KEY"Daily series for a window, and the log filtered to PAYG requests:
curl "https://kenari.id/v1/usage/daily?start=1788300000&end=1790892000" \ -H "Authorization: Bearer $KENARI_API_KEY"
curl "https://kenari.id/v1/usage/log?billing_kind=payg&limit=100&page=0" \ -H "Authorization: Bearer $KENARI_API_KEY"Python
Section titled “Python”This script sums this month’s spend per model. It reads the summary from the first day of the month in UTC, and adds up the billing kinds of each model.
import osfrom collections import defaultdictfrom datetime import datetime, timezone
import requests
now = datetime.now(timezone.utc)month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
response = requests.get( "https://kenari.id/v1/usage", headers={"Authorization": f"Bearer {os.environ['KENARI_API_KEY']}"}, params={"start": int(month_start.timestamp())},)response.raise_for_status()usage = response.json()
per_model = defaultdict(int)for row in usage["data"]: per_model[row["model"]] += row["cost_micro_idr"]
for model, micro in sorted(per_model.items(), key=lambda item: -item[1]): print(f"{model}: Rp {micro / 1_000_000:,.0f}")print("Total:", f"Rp {usage['total']['cost_micro_idr'] / 1_000_000:,.0f}")JavaScript
Section titled “JavaScript”const now = new Date();const monthStart = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1) / 1000;
const response = await fetch(`https://kenari.id/v1/usage?start=${monthStart}`, { headers: { Authorization: `Bearer ${process.env.KENARI_API_KEY}` },});if (!response.ok) throw new Error(await response.text());const usage = await response.json();
const perModel = {};for (const row of usage.data) { perModel[row.model] = (perModel[row.model] ?? 0) + row.cost_micro_idr;}
for (const [model, micro] of Object.entries(perModel).sort((a, b) => b[1] - a[1])) { console.log(model, Math.round(micro / 1_000_000));}Billing
Section titled “Billing”Reading usage is free. See How billing works.
Errors
Section titled “Errors”| Status | Code | When |
|---|---|---|
| 400 | bad_request | A parameter is not valid: a value that is not an integer, a negative start or end, start after end, a range that is not one of the four presets (when no start or end is given), a window longer than 366 days on /v1/usage or /v1/usage/daily, a billing_kind that is not one of the six values, a negative page, page times limit over 10,000, or an api_key_id that is not the restricted key’s own id. The param field names the parameter and message says what is wrong. |
| 401 | None | The key is missing, invalid or expired. The body is a short plain-text message, not JSON. |
| 403 | shared_key_not_allowed | The key is a shared API key. |
| 429 | rate_limit_exceeded | More than 60 usage requests in a minute for the account. There is no Retry-After header. |
| 500 | internal_error | Reading the usage failed in kenari. Retry once. |
The body is the OpenAI format. A 400 has type set to invalid_request_error and param set to the name of the parameter. See Errors for the error format and the remaining codes.