Authentication
One key, the header that carries it, and the three things the platform records about a key: what it is called, what funds it and what it is allowed to reach.
Fast
The key travels in one header, and that one header covers the whole surface:
export KUMO_API_KEY="kumo_sk_..."
curl https://api.kumorouter.com/v1/chat/completions \
-H "Authorization: Bearer $KUMO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "<model>",
"max_tokens": 16,
"messages": [{ "role": "user", "content": "reply with OK" }]
}'Anthropic clients and SDKs send the key differently — bare, in the x-api-key header. Both Anthropic-native operations accept it that way as readily as the bearer header, so such a client only changes its address and its key.
The whole first call → Check a key →
The header
The key travels as a bearer token in the Authorization header and in nothing else — never in a query string, never in a cookie, never in a body. A query string ends up in access logs and browser history; a header does not.
Every operation that reaches a model declares that one carrier, and the same key opens all of them: there is a single credential for the whole surface rather than one per protocol. A key is issued to an organization, and every call is counted and charged to that organization.
# Keep the key in the environment, never in the source tree.
export KUMO_API_KEY="kumo_sk_..."
# Every call carries it in one header, and nowhere else.
curl https://api.kumorouter.com/v1/chat/completions \
-H "Authorization: Bearer $KUMO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "<model>",
"max_tokens": 16,
"messages": [{ "role": "user", "content": "reply with OK" }]
}'The Anthropic carrier
The two Anthropic-native operations — POST /v1/messages and POST /v1/messages/count_tokens — accept a second carrier for the same key: presented bare in the x-api-key header, with no Authorization header at all. That is the carrier the native Anthropic API uses and therefore the only one its SDKs send, so a client written against that API authenticates here without a single edit.
The header is declared on those two operations and nowhere else: it is those clients' carrier, not a second way into the rest of the surface. Presenting both headers is legal. If they disagree, the request is not refused for it: an Authorization header that is present and well formed is the one that authenticates, and the bare header is not read at all. A server that had to choose between two secrets would be guessing which one you meant. The key itself, the lookup behind it and the one refusal below are the bearer header's, unchanged.
curl https://api.kumorouter.com/v1/messages \
-H "x-api-key: $KUMO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "<model>",
"max_tokens": 128,
"messages": [{ "role": "user", "content": "Explain tokens in one line." }]
}'What a key looks like
A key is that prefix followed by a random tail, and the platform never holds it in that form: what it keeps is a hash of the key together with the leading characters of the random part and the last four.
The console shows the whole key exactly once, on the screen that mints it, with a control that copies it. Nothing afterwards can read it back: no screen, no operation, no support request.
A repeat of the same creation call answers with the key's metadata and no secret at all. That is why the remedy for a lost key is a new key rather than a recovery.
Everywhere else a key is named by the two fragments it is safe to show — those leading characters and that tail — plus the name you gave it and a version number.
That version number is the token an edit compares against. It moves when the key's metadata is edited and deliberately not when the key is merely used: otherwise a key under traffic would invalidate the version its owner is holding on a console form several times a second.
Those same two fragments are what the identity echo answers with, so a screenshot of a key check leaks nothing.
Mint a key → Read a key back →
What a key may reach
A key carries an optional scope on three axes. Absence is permission: an axis that is missing means "every one of them", so a key with no axis at all is unrestricted, and the key check says so in words rather than showing you an empty list.
modality_codesWhich kinds of work the key may ask for — text generation, embeddings, image generation.model_idsThe exact models the key may name, when it must be narrower than a modality.vendor_idsThe suppliers behind those models, for a key tied to one of them.bindingThe key's original binding, set when the key is created and immutable afterwards. Whether the key spends from the account package or from the balance is chosen separately — in the console, and that choice can be changed.A call that names something outside those lists is refused rather than routed: a scope is a whitelist, not a preference. The catalog of everything that can be named lives on the price list, and the gateway will list the IDs it carries at https://api.kumorouter.com/v1/models.
One refusal, and one only
Every refusal of a presentation is the same 401. Five cases are answered alike:
- no header at all;
- a malformed key;
- an unknown key;
- a revoked key;
- an expired key.
The answer never says which of the five it was. That is deliberate: telling them apart would make the surface an oracle over key material, where anyone holding a list of guesses could learn which of them exist.
So a 401 is one instruction rather than five — present a working key. Which of your own keys is live, revoked or expired is a question the console answers; the gateway will not, whoever is asking.
Check a key you hold → Every status the gateway answers with →