Skip to content
kenari.

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/usage
GET /v1/usage/daily
GET /v1/usage/log

All 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.

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.

ParameterTypeEndpointsDescription
startintegerallStart of the window, in Unix seconds, inclusive.
endintegerallEnd of the window, in Unix seconds, inclusive. A value in the future is treated as now.
rangestringallA preset window that ends now: today (since 00:00 UTC), 24h, 7d or 30d. The default is 30d.
modelstringallOnly this exact model id.
api_key_idstringallOnly requests made with this key id. See Who can read what for restricted keys.
billing_kindstring/v1/usage/logOnly this billing kind: payg, plan, coupon, token_alloc, free or byok.
pageinteger/v1/usage/logPage number, starting at 0. The default is 0.
limitinteger/v1/usage/logRequests per page. The default is 50. A value outside 1 to 200 is moved to the nearest limit, not rejected.

All three endpoints resolve the window in the same way.

  • With no start, end or range, the window is the last 30 days.
  • start and end win over range. When either is present, range is ignored, even if its value is not valid.
  • With only start, the window ends now. With only end, it starts 30 days before end.
  • end is clamped to now. start and end must not be negative, and start must not be after end.
  • /v1/usage and /v1/usage/daily accept a window that touches at most 366 UTC days. A longer window is a 400. /v1/usage/log has 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_kind says what paid for a request.

ValueMeaning
paygPAYG, paid from your balance.
planCovered by your plan quota.
couponCovered by a coupon.
token_allocCovered by a token allocation.
freeA free model.
byokServed 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_idr is what the requests took from your balance. Plan, coupon, free and BYOK usage is 0 here, because none of it is paid from balance.
  • catalog_micro_idr is 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 or 0.

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.

FieldTypeDescription
objectstringAlways usage.summary.
startintegerStart of the window, in Unix seconds.
endintegerEnd of the window, in Unix seconds.
dataarrayOne row per model and billing kind. Empty when there is no usage.
data[].modelstringThe model id.
data[].billing_kindstringOne of the billing kinds above.
data[].requestsintegerNumber of requests.
data[].prompt_tokensintegerInput tokens.
data[].completion_tokensintegerOutput tokens.
data[].cached_prompt_tokensintegerInput tokens read from the cache.
data[].cache_write_prompt_tokensintegerInput tokens written to the cache.
data[].cost_micro_idrintegerAmount taken from your balance, in micro-Rupiah.
data[].catalog_micro_idrintegerValue at list price, in micro-Rupiah.
totalobjectThe 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
}
}

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.

FieldTypeDescription
objectstringAlways usage.daily.
startintegerStart of the window, in Unix seconds.
endintegerEnd of the window, in Unix seconds.
dataarrayOne row per UTC day.
data[].datestringThe UTC day, as YYYY-MM-DD.
data[].requestsintegerNumber of requests on that day.
data[].cost_micro_idrintegerAmount 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 }
]
}

GET /v1/usage/log returns single requests, newest first. It supports every filter, including billing_kind. Plan usage is reported as plan.

FieldTypeDescription
objectstringAlways list.
startintegerStart of the window, in Unix seconds.
endintegerEnd of the window, in Unix seconds.
dataarrayThe requests on this page.
data[].created_atintegerWhen the request was made, in Unix seconds.
data[].modelstringThe model id.
data[].vendorstringThe maker of the model.
data[].api_key_idstring or nullThe id of the key that made the request. null when there is none.
data[].billing_kindstringOne of the billing kinds above.
data[].prompt_tokensintegerInput tokens.
data[].completion_tokensintegerOutput tokens.
data[].cached_prompt_tokensintegerInput tokens read from the cache.
data[].cache_write_prompt_tokensintegerInput tokens written to the cache.
data[].cost_micro_idrintegerAmount taken from your balance, in micro-Rupiah.
data[].catalog_micro_idrintegerValue at list price, in micro-Rupiah.
pageintegerThe page number that was used.
limitintegerThe page size that was used, after clamping.
has_morebooleantrue when another page follows.
totalintegerNumber 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.

  • 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_id out, or set it to the key’s own id. Any other id is a 400.
  • A shared API key cannot read usage at all. It gets 403 and shared_key_not_allowed.

See Authentication and API keys for how to restrict a key.

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.

Terminal window
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:

Terminal window
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"

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 os
from collections import defaultdict
from 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}")
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));
}

Reading usage is free. See How billing works.

StatusCodeWhen
400bad_requestA 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.
401NoneThe key is missing, invalid or expired. The body is a short plain-text message, not JSON.
403shared_key_not_allowedThe key is a shared API key.
429rate_limit_exceededMore than 60 usage requests in a minute for the account. There is no Retry-After header.
500internal_errorReading 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.