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.

GET/v1/usage

Every 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"
200 OK — an account on a plan with spend windows
{
  "object": "usage",
  "plan": "ultra",
  "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#

FieldTypeDescription
objectstringAlways “usage”.
planstring | nullPlan key currently in force, for example ultra. null means pay-as-you-go — no plan, or a plan whose term has already ended.
windowsarraySpend windows the plan enforces. Empty on PAYG, where the wallet is the only limit.
free_tokensobjectToday's allowance on free-tier models.
walletobject | nullWallet balance in USD. null if no wallet has been created yet, which is the case until the first top-up or credit.

windows[]

FieldTypeDescription
kindstringshort or long — the two windows a plan can define. Read the length from window_sec rather than assuming which is which.
window_secintegerWindow length in seconds. 18000 is five hours; 604800 is seven days.
spent_usdstringSpent inside the current window.
cap_usdstringThe cap for this window.
remaining_usdstringcap − spent, floored at zero. Never negative.
resets_in_secintegerSeconds 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

FieldTypeDescription
used_todayintegerFree-model tokens used since 00:00 UTC.
limit_per_dayinteger | nullToday's allowance. null means no free-token cap applies to this account.
remaininginteger | nulllimit_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.

200 OK — pay-as-you-go
{
  "object": "usage",
  "plan": 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.

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_usd approaching zero with a large resets_in_sec is 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 429 already carries Retry-After, and reacting to the real answer beats predicting it. See Rate limits.
Stop a batch job before it starts failing
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#

FieldTypeDescription
401authentication_errorMissing, unknown or disabled key.
500api_errorUnexpected. 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.