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

```text
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 that an Owner or Admin holds reads the usage of the whole account, with every API key on it. See [Who can read what](#who-can-read-what).

## 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](#who-can-read-what) for restricted keys. |
| `user_id` | string | all | Only the usage of this member of the workspace: the requests made with the keys that person holds there, and their Studio use. It is the `user_id` from the members list of the [Workspace API](/en/docs/workspace-api). On `/v1/usage/log`, pass it again with every `page`. See [Who can read what](#who-can-read-what). |
| `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

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

## Money

Every amount is an integer in micro-Rupiah, which is Rupiah times 1,000,000. Rp 1 is `1000000`. See [How billing works](/en/docs/billing). 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`.

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

```json
{
  "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

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

```json
{
  "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

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

```json
{
  "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

- A key without restrictions that an Owner or Admin holds reads the usage of the whole account, and can narrow it with `api_key_id` or `user_id`.
- A key without restrictions that a Member holds reads only the usage of that person.
- 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`.

`user_id` follows these rules:

- A key held by an Owner or Admin can pass the `user_id` of any member of its workspace. An id that is not in the workspace returns empty results, not an error.
- A key held by a Member can pass only the `user_id` of its own holder. Any other id gets `403` and `permission_denied`.
- A shared key or a restricted key that passes `user_id` gets `403`, with `shared_key_not_allowed` or `restricted_key_not_allowed`.
- A `user_id` that is not 1 to 64 characters of letters, digits, hyphen or underscore gets `400` and `bad_request`.

See [Authentication and API keys](/en/docs/authentication) for how to restrict a key.

## 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 `Retry-After` is 60 seconds. Wait that long 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](/en/docs/rate-limits).

## Examples

### curl

```bash
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:

```bash
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

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.

```python
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}")
```

### JavaScript

```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

Reading usage is free. See [How billing works](/en/docs/billing).

## 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, an `api_key_id` that is not the restricted key's own id, or a `user_id` that is not 1 to 64 characters of letters, digits, hyphen or underscore. 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. |
| 403 | `restricted_key_not_allowed` | A restricted key passed `user_id`. |
| 403 | `permission_denied` | A key held by a Member passed a `user_id` that is not its holder's own. |
| 429 | `rate_limit_exceeded` | More than 60 usage requests in a minute for the account. `Retry-After` is 60 seconds. |
| 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](/en/docs/errors) for the error format and the remaining codes.

## See also

- [How billing works](/en/docs/billing)
- [Account overview](/en/docs/account)
- [Account balance](/en/docs/account-balance)
- [Account quota](/en/docs/account-quota)
- [Rate limits](/en/docs/rate-limits)
- [Authentication and API keys](/en/docs/authentication)
- [Workspace API](/en/docs/workspace-api)
