Using the API
Usage & limits
What is left before something stops you: the spend windows on your plan, today's free token allowance, and your wallet balance — all readable with the key you already have. A second endpoint on the same path returns what you have already spent, bucketed over time.
/v1/usageEvery number here comes from the same counters the gateway checks when it decides whether to accept a request. What this endpoint shows is what will actually block you — not a separate report that can drift away from it.
curl https://api.xkiro.com/v1/usage \
-H "Authorization: Bearer $XKIRO_API_KEY"{
"object": "usage",
"plan": "ultra",
"user": {
"name": "Ada Lovelace",
"email": "[email protected]",
"avatar_url": "https://cdn.xkiro.com/avatars/…/a1b2c3.webp"
},
"windows": [
{
"kind": "short",
"window_sec": 18000,
"spent_usd": "0.000000",
"cap_usd": "200.000000",
"remaining_usd": "200.000000",
"resets_in_sec": 5005
},
{
"kind": "long",
"window_sec": 604800,
"spent_usd": "0.000356",
"cap_usd": "1320.000000",
"remaining_usd": "1319.999644",
"resets_in_sec": 199405
}
],
"free_tokens": {
"used_today": 412030,
"limit_per_day": 300000000,
"remaining": 299587970
},
"wallet": {
"balance_usd": "683.950000",
"held_usd": "0.000000"
}
}Authentication#
Requires a key. Both header styles work, the same as everywhere else on the API — see Authentication.
Authorization: Bearer sk-xt-…x-api-key: sk-xt-…
The figures are for the account the key belongs to, not for that key alone. Two keys on the same account return the same numbers. Per-key spending limits are a separate control, set in the console.
Free to call
This endpoint costs nothing, consumes no tokens, and does not count against your rate limit or spend windows. Reading your remaining balance is never the thing that exhausts it.
Response fields#
| Field | Type | Description |
|---|---|---|
object | string | Always “usage”. |
plan | string | null | Plan key currently in force, for example ultra. null means pay-as-you-go — no plan, or a plan whose term has already ended. |
user | object | null | Who the key belongs to — name, email and avatar_url, each nullable. Enough for a client to show which account it is signed in as instead of a blank field. null if the account record cannot be read. |
windows | array | Spend windows the plan enforces. Empty on PAYG, where the wallet is the only limit. |
free_tokens | object | Today's allowance on free-tier models. |
wallet | object | null | Wallet balance in USD. null if no wallet has been created yet, which is the case until the first top-up or credit. |
windows[]
| Field | Type | Description |
|---|---|---|
kind | string | short or long — the two windows a plan can define. Read the length from window_sec rather than assuming which is which. |
window_sec | integer | Window length in seconds. 18000 is five hours; 604800 is seven days. |
spent_usd | string | Spent inside the current window. |
cap_usd | string | The cap for this window. |
remaining_usd | string | cap − spent, floored at zero. Never negative. |
resets_in_sec | integer | Seconds until this window rolls over and the spend resets to zero. |
Amounts are strings, not numbers
USD amounts are fixed-point strings with six decimals ("0.000356") so that small per-request costs survive JSON parsing intact. Parse them with a decimal type, or compare them as strings — parseFloat is fine for display and a poor idea for accounting.
free_tokens
| Field | Type | Description |
|---|---|---|
used_today | integer | Free-model tokens used since 00:00 UTC. |
limit_per_day | integer | null | Today's allowance. null means no free-token cap applies to this account. |
remaining | integer | null | limit_per_day − used_today, floored at zero. null whenever limit_per_day is null. |
Free tokens and paid spend are separate budgets
Exhausting the free-token allowance does not touch your wallet or your plan windows — paid models keep working normally. They are two independent limits, which is why they are two separate objects in this response rather than one combined number.
Pay-as-you-go accounts#
With no plan, windows is empty and the wallet is what matters. A request is rejected with 402 when the balance cannot cover it — see Error codes.
{
"object": "usage",
"plan": null,
"user": { "name": "Ada Lovelace", "email": "[email protected]", "avatar_url": null },
"windows": [],
"free_tokens": { "used_today": 124035, "limit_per_day": 5000000, "remaining": 4875965 },
"wallet": { "balance_usd": "4.812300", "held_usd": "0.150000" }
}held_usd is money reserved for requests currently in flight. A generation is held at an estimate when it starts and settled at the real cost when it finishes, so this rises and falls on its own during heavy use — it is not a deduction.
Spend history#
/v1/usage/historyWhere /v1/usage answers “what is left?”, this answers “what has gone?” — requests, tokens and spend bucketed over the last day, week or month. It reads the same request log the console charts are drawn from, so a client can render its own usage graph without scraping the dashboard.
curl "https://api.xkiro.com/v1/usage/history?period=week" \
-H "Authorization: Bearer $XKIRO_API_KEY"Query parameters
| Field | Type | Description |
|---|---|---|
period | string | day (default) — 24 hourly buckets. week — 7 daily buckets. month — 30 daily buckets. Anything else falls back to day rather than returning 400; a read-only chart endpoint should not break a page over a typo in a query string. |
Three fixed windows, not an arbitrary range
There is deliberately no start/end pair here. This endpoint has no rate limit, for the same reason /v1/usage has none, and an open-ended range on an unmetered endpoint is a full table scan waiting to be requested. Three fixed windows keep the query cost bounded and known in advance. The console has the arbitrary-range version.
{
"object": "usage.history",
"period": "week",
"bucket": "day",
"points": [
{ "ts": "2026-09-05T00:00:00.000Z", "requests": 6, "tokens": 10538, "spend_usd": "0.033062" },
{ "ts": "2026-09-06T00:00:00.000Z", "requests": 5, "tokens": 1224, "spend_usd": "0.006286" },
{ "ts": "2026-09-07T00:00:00.000Z", "requests": 5, "tokens": 3576, "spend_usd": "0.009392" },
{ "ts": "2026-09-08T00:00:00.000Z", "requests": 0, "tokens": 0, "spend_usd": "0.000000" },
{ "ts": "2026-09-09T00:00:00.000Z", "requests": 3, "tokens": 1519, "spend_usd": "0.000985" },
{ "ts": "2026-09-10T00:00:00.000Z", "requests": 15, "tokens": 62828, "spend_usd": "0.124429" },
{ "ts": "2026-09-11T00:00:00.000Z", "requests": 7, "tokens": 84535, "spend_usd": "0.000000" }
],
"total": { "requests": 41, "tokens": 164220, "spend_usd": "0.174153" }
}Response fields
| Field | Type | Description |
|---|---|---|
object | string | Always “usage.history”. |
period | string | The window actually served — echoed back, so you can tell when a bad value fell back to day. |
bucket | string | hour or day — what one point covers. Read it rather than inferring the step from the gap between two ts values. |
points | array | One entry per bucket, oldest first. Empty buckets are included with zeros, so the array length is always 24, 7 or 30 and a chart can map it straight onto an axis without filling gaps itself. |
total | object | Sum across the returned points — requests, tokens and spend_usd. |
points[]
| Field | Type | Description |
|---|---|---|
ts | string | Start of the bucket, ISO-8601 in UTC. Convert for display if you need local time; the buckets themselves are cut on UTC boundaries. |
requests | integer | Requests logged in this bucket, successful or not. |
tokens | integer | Total tokens for the bucket — input, output, cache reads and reasoning together. Not the sum of two visible columns: on long sessions cache reads can be the overwhelming majority, so a chart built from prompt + completion alone reads far lower than the figure you are billed against. |
spend_usd | string | Spend attributed to the bucket, fixed-point with six decimals — same string format and same caveat as everywhere else on this page. "0.000000" with non-zero tokens means free-tier models: real usage, no charge. |
The last bucket is still open
The newest point is the hour or day currently in progress, not a closed one. It will keep rising until the bucket ends. That is deliberate — dropping it would hide the most recent activity, which is the part most worth looking at — but it means the last point is not comparable with the ones before it, and a bar chart will always show a short final column near the start of a bucket.
Same account scope and same auth as /v1/usage: the numbers cover the account the key belongs to, not that one key. Also free to call, also unmetered.
Using it well#
- Poll sparingly. Once a minute is plenty for a dashboard. Nothing here changes faster than your own traffic changes it.
- Check before a batch, not before every call. Read it once at the start of a long job to decide whether to run at all; per-request checks double your round trips to learn something the next request would have told you anyway.
- Alert on the window, not the wallet, if you are on a plan.
remaining_usdapproaching zero with a largeresets_in_secis the situation worth warning about — that is hours of waiting, whereas a low wallet is a top-up. - Do not use it to build your own rate limiter. A
429already carriesRetry-After, and reacting to the real answer beats predicting it. See Rate limits.
const res = await fetch("https://api.xkiro.com/v1/usage", {
headers: { Authorization: `Bearer ${process.env.XKIRO_API_KEY}` },
});
const usage = await res.json();
const shortWindow = usage.windows.find((w: { kind: string }) => w.kind === "short");
if (shortWindow && Number(shortWindow.remaining_usd) < 1) {
throw new Error(
`Only $${shortWindow.remaining_usd} left in this window; it resets in ` +
`${Math.ceil(shortWindow.resets_in_sec / 60)} minutes.`,
);
}Errors#
| Field | Type | Description |
|---|---|---|
401 | authentication_error | Missing, unknown or disabled key. |
500 | api_error | Unexpected. Individual sections degrade rather than fail — if a counter cannot be read, that part comes back at zero or null instead of failing the whole response. |
