What a leaked API key can actually do
A leaked key can spend your entire prepaid balance and nothing more: keys are minted with unlimited_quota at the key level on purpose, so the account balance is the only ceiling, and at zero the API returns 429 with code insufficient_credits. It cannot top up, mint another key, read your balance, or move money -- the payments integration is collection-only. The one thing that raises the ceiling is auto-recharge, which adds up to three $50 charges per rolling day.
Shown once, because it is fetched once
The creation response carries the plaintext key and a shown_once: true field alongside it. That field is in the payload rather than only in the docs, because a client that assumes it can re-read the value later will silently lose the key and only find out when a customer asks.
The listing endpoint never returns plaintext again. It returns a masked form -- an sk- prefix, four characters, eight asterisks -- which is enough to tell two keys apart in a table and not enough to authenticate with. There is no second endpoint, no support process and no admin screen that recovers it, because the value is not held anywhere that could serve one.
This is a weaker property than a salted hash and it is worth stating precisely rather than dressing up. The gateway holds the key material; what we do not do is expose a path that hands it back. If you have lost a key, the operation is not recovery. It is rotation.
The key you hold and the key the gateway stores are different strings
The gateway stores keys without a prefix and strips a leading sk- during validation. We present them prefixed, because every OpenAI-compatible client, log scrubber and secret scanner in the ecosystem recognises that shape and a bare token defeats all three.
The practical consequence is that both forms authenticate. If a key appears in a log without its prefix, treat it as compromised anyway -- the missing sk- does not make it inert, and a secret scanner that only matches the prefixed form will not have flagged it.
A leaked key has exactly one blast radius: the balance
Keys here are created with unlimited_quota set at the key level, and a remaining quota of zero, deliberately. The reasoning is in the code: the account balance is what actually stops spend, and a per-key cap layered on top of it would silently shadow the balance and produce an insufficient-quota error on a key while the account still had credit. Per-key caps are a feature to build with a UI, not a default to guess at.
So the exposure of a leaked key is precisely your current balance, and the balance is a hard ceiling rather than an overdraft -- at zero the API refuses, and the refused request is not charged. Here is what that ceiling is worth in tokens at each rung of the top-up ladder, on the most and least expensive text models we serve.
| Balance | GLM-5.2 output tokens ($2.55/M) | Kimi K3 output tokens ($9.00/M) |
|---|---|---|
| $30 | 11.8M | 3.3M |
| $50 | 19.6M | 5.6M |
| $100 | 39.2M | 11.1M |
| $200 | 78.4M | 22.2M |
| $500 | 196.1M | 55.6M |
Auto-recharge is the thing that raises that ceiling
If automatic top-up is on, the balance is no longer the whole story, and this is the single most important paragraph on this page for anyone who has enabled it. The defaults fire a $50 charge when the balance drops below $10, capped at three charges per rolling UTC day with a five-minute cooldown between them.
That means a key leaked into a public repository can cost up to $150 a day beyond whatever balance was already on the account, for as long as the card keeps approving. The daily cap is not decoration -- it is the difference between a bounded incident and an unbounded standing instruction against a card -- but it is a cap, not a zero.
Two other guards limit how long that can run. Auto-recharge disables itself after three consecutive declines, which is what stops a fraud-flagged card being retried into a merchant-account problem. And a strong-authentication decline latches it off until the customer re-authenticates, because that is a failure retrying can never clear.
If you suspect a leak and you are not sure which key it was, turn auto-recharge off first and revoke second. Revocation stops the key; only turning off auto-recharge stops the card.
What a leaked key cannot do
The surface a key reaches is inference, and only inference. Everything in the account -- keys, balance, billing, profile -- sits behind a browser session rather than behind the key, and the two authenticate against different systems entirely.
| Action | Reachable with a leaked key? | Why not |
|---|---|---|
| Call any model, up to the balance | Yes | This is what the key is for |
| Read your balance or usage history | No | The account endpoints require a browser session, not a bearer key |
| Mint another API key | No | Key creation runs as the user against the gateway, using a credential derived from the account identity |
| Revoke your other keys | No | Same path, same credential |
| Add credit to the account | No | Crediting happens only in the payment webhook, which no customer-driven path can reach |
| Withdraw or transfer money | No | The payments integration is collection-only. There is no payout or transfer surface at all |
| Spend past the balance | No | 429 insufficient_credits, and the refused request is not charged |
What rotation actually has to do
Rotation is not one action, and the ordering is the part people get wrong. Revocation here is immediate -- there is no grace period, no overlapping validity window, no deprecation state on a key. A key that is revoked stops working on the next request.
So the sequence is: mint the new key, deploy it everywhere the old one runs, verify with one real request that the new key returns a usage block, then revoke the old one. Revoking first produces an outage of exactly the length of your deploy.
Revocation itself is authorised by the credential rather than by a lookup. The delete runs as the owning account against the gateway, so the gateway refuses an id belonging to somebody else. That is a meaningfully stronger property than an ownership check written in our own code, because it is not a check anyone can forget to write.
# 1. Mint. The plaintext exists for this one response.
curl -X POST /api/keys -d '{"name":"worker-2026-09"}'
# { "id": 41, "name": "worker-2026-09", "key": "sk-...", "shown_once": true }
# 2. Deploy, then verify against the real API before revoking anything.
curl https://router.xark.io/api/v1/chat/completions \
-H "Authorization: Bearer sk-<new>" \
-H "Content-Type: application/json" \
-d '{"model":"z-ai/glm-5.2","messages":[{"role":"user","content":"ping"}]}' \
| jq .usage
# 3. Only now revoke the old one. There is no grace period.
curl -X DELETE "/api/keys?id=38"
One key per project, because the organisation headers do nothing
OpenAI-Organization and OpenAI-Project are accepted and discarded. There is nothing behind them here, and rejecting them would break integrations that set them by default. The separation they would have given you comes from issuing a key per project instead.
That is not a downgrade, because spend is attributed per key at the gateway regardless of the fact that it is not capped per key. Every key carries its own used_quota, and every consumption row in the usage log carries the name of the key that produced it alongside the model and both token counts. A key named after a service is therefore a cost centre, and a key named Default is a line item nobody can explain three months later.
Name keys for where they run, not for who created them. worker-2026-09 and ci-eval are useful in a usage log; My key is not.
The account behind your account
Every customer has a gateway account they never see, and its credentials are derived rather than stored. The username is an HMAC of the account id, hex, prefixed with u; the password is a separate HMAC under a server-held secret, base64url, twenty characters -- roughly 120 bits, and exactly the field's ceiling, because the gateway validates usernames at 20 characters and passwords between 8 and 20.
Two details in that derivation are worth naming because they are the kind that quietly go wrong. The password derivation carries its own label, so knowing the username reveals nothing about the password even though both come from the same account id. And the username is hashed rather than truncated: an account id is about 29 characters against a 20-character field, so slicing would collide for any two customers sharing a prefix, and the failure mode of that collision is one customer being handed another customer's account. That is not a bug worth risking to save a hash.
It also means provisioning is idempotent by construction. A second attempt collides on the gateway's own uniqueness constraint rather than creating a duplicate, so two concurrent first requests -- a dashboard firing two fetches on mount is enough -- are safe, and the loser simply reads the row the winner made.
A checklist for the day it happens
Turn off auto-recharge before you do anything else. Revoking the key stops the spend; only this stops the card.
Revoke every key you cannot positively account for. There is no cost to over-revoking except a deploy, and the alternative is guessing.
Read the usage log by key name. Consumption rows carry the key that produced them, so the shape of a compromise is visible without asking us.
Do not assume the missing sk- prefix protected you. Both forms authenticate.
Set a lower balance deliberately if the key runs somewhere you do not fully control. The balance is the ceiling, so choosing it is the control -- a $30 rung is a $30 worst case.
AI Token Router is an OpenAI-compatible gateway for open-weight models. Every rate on the pricing page is printed next to the model’s official rate, so the numbers in this post are checkable rather than claimed.
Get an API keyRelated
- Rate limits, concurrency and backoff
Five independent per-endpoint buckets, a fixed window that allows 120 requests in two seconds, and the field that says whether retrying will help.
- Streaming: who pays when nobody listens
The SSE frames in order, the one frame that carries usage, and what actually happens to the bill when a client hangs up mid-completion.
- Prepaid credits vs postpaid invoicing
A prepaid balance is a hard ceiling a runaway agent cannot exceed. What that protects you from, what it costs at procurement, and the fees across the market.