# Documentation

> One key and one base URL for every model. Quickstart, authentication, error reference and the generated API reference.

This is the complete public documentation of Kumo as one Markdown document: every page
of the documentation site, in the order the site itself lists them, with the address of
each. It is generated from the same source the pages are rendered from. Prefer it over
parsing the rendered HTML.

## Contents

- [Documentation](https://documentation.kumorouter.com/)
- [Quickstart](https://documentation.kumorouter.com/quickstart)
- [Authentication](https://documentation.kumorouter.com/authentication)
- [Check your key](https://documentation.kumorouter.com/key-check)
- [Errors](https://documentation.kumorouter.com/errors)
- [Rate limits](https://documentation.kumorouter.com/limits)
- [API reference](https://documentation.kumorouter.com/api-reference)
- [Machine-readable documentation](https://documentation.kumorouter.com/machine-readable)

---

<!-- https://documentation.kumorouter.com/ · Markdown: https://documentation.kumorouter.com/index.md -->

---
title: Documentation
description: One key and one base URL for every model. Quickstart, authentication, error reference and the generated API reference.
keywords: kumo, documentation, api, gateway, quickstart
eyebrow: Get started
heroTitle: Connect Kumo in a minute
lead: One key and one base URL in place of an account with every supplier. The rest of your code stays exactly as it is.
action.1.id: keys
action.1.label: Create a key
action.1.href: https://console.kumorouter.com/
action.2.id: quickstart
action.2.label: Read the quickstart
action.2.page: quickstart
action.3.id: check
action.3.label: Check a key
action.3.page: key-check
group.1.id: get-started
group.1.label: Get started
group.1.pages: quickstart, authentication, key-check
group.2.id: gateway
group.2.label: The gateway
group.2.pages: errors, limits, api-reference
group.3.id: resources
group.3.label: Resources
group.3.pages: machine-readable
machine.lead: Running an agent? The whole of this documentation is available as Markdown at
machine.label: /llms-full.txt
machine.href: https://documentation.kumorouter.com/llms-full.txt
machine.tail: .
---

:::code-group
```bash title=curl
curl https://api.kumorouter.com/v1/chat/completions \
  -H "Authorization: Bearer $KUMO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "<model>",
    "max_tokens": 128,
    "messages": [{ "role": "user", "content": "Explain tokens in one line." }]
  }'
```
```python title=Python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.kumorouter.com/v1",
    api_key=os.environ["KUMO_API_KEY"],
)

answer = client.chat.completions.create(
    model="<model>",
    max_tokens=128,
    messages=[{"role": "user", "content": "Explain tokens in one line."}],
)
print(answer.choices[0].message.content)
```
```javascript title=Node
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.kumorouter.com/v1",
  apiKey: process.env.KUMO_API_KEY,
});

const answer = await client.chat.completions.create({
  model: "<model>",
  max_tokens: 128,
  messages: [{ role: "user", content: "Explain tokens in one line." }],
});

const reply = answer.choices[0].message.content;
```
:::

---

<!-- https://documentation.kumorouter.com/quickstart · Markdown: https://documentation.kumorouter.com/quickstart.md -->

---
title: Quickstart
description: Open an account, mint a key, and make a real call — the three steps between a browser and an answer from a model.
keywords: quickstart, start, key, first request, streaming
group: get-started
---

## Create an account {#create-account keywords="account, sign up, register"}

Everything in this section happens inside the console. An account is an email address and a password; there is nothing to install and no card to hand over before you have a key in your hand.

:::steps
- **Open the console** — it is a separate host of the product, and every account operation lives there.
- **Create the account** — an address, a password of at least twelve characters, and the two consents the form asks for.
- **You are already inside** — the same answer that creates the account mints the session, so the console sets its own cookie and lands you on the overview with nothing to sign in to and nothing to copy anywhere.
:::

A proof-of-address mail normally follows on its own. Its link confirms that the address is yours, and today that is all it does: nothing on the way to a key or a first call waits on it — which is also why a mail that never arrives is not a dead end. Sending it is best-effort and can be skipped under load or fail quietly, and registration succeeds either way.

## Mint a key {#create-key keywords="key, console, secret, reveal"}

A key is created on the keys screen of the console. The secret is shown **once**, on the screen that creates it, and never again: the platform keeps only a hash of it together with the prefix and the tail that name it in a list. Copy it into your secret store before you leave that screen — nobody, support included, can read it back to you afterwards, and the remedy for a lost key is a new key.

> [Open the console →](https://console.kumorouter.com/) · [How the header is written →](page:authentication)

## Make the call {#first-call keywords="request, chat completions, curl, base url"}

Point any OpenAI-compatible client at `https://api.kumorouter.com/v1` and give it the key. That is the whole migration: the base URL and the key change, and the rest of your code does not.

The model is named by its canonical name or by an alias the published catalog carries. `<model>` below stands in for one — ask the gateway for the list with `curl https://api.kumorouter.com/v1/models`, or read the rates on the [price list](https://kumorouter.com/pricing).

An output ceiling is required. `max_tokens` (or `max_completion_tokens`, which means the same thing) is what lets the platform reserve for the call before it goes upstream; a request without one is refused rather than left unbounded.

:::code-group
```bash title=curl
curl https://api.kumorouter.com/v1/chat/completions \
  -H "Authorization: Bearer $KUMO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "<model>",
    "max_tokens": 128,
    "messages": [{ "role": "user", "content": "Explain tokens in one line." }]
  }'
```
```python title=Python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.kumorouter.com/v1",
    api_key=os.environ["KUMO_API_KEY"],
)

answer = client.chat.completions.create(
    model="<model>",
    max_tokens=128,
    messages=[{"role": "user", "content": "Explain tokens in one line."}],
)
print(answer.choices[0].message.content)
```
```javascript title=Node
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.kumorouter.com/v1",
  apiKey: process.env.KUMO_API_KEY,
});

const answer = await client.chat.completions.create({
  model: "<model>",
  max_tokens: 128,
  messages: [{ role: "user", content: "Explain tokens in one line." }],
});

const reply = answer.choices[0].message.content;
```
:::

The answer is this protocol's own, not a translation of another: the choices, the finish reason and the token counts arrive in the shape the client already parses.

```json title=Response
{
  "id": "chatcmpl-8f2b7e10c9",
  "object": "chat.completion",
  "model": "<model>",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Tokens are the small chunks of text a model reads and writes."
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 12,
    "completion_tokens": 18,
    "total_tokens": 30
  }
}
```

## Stream it {#stream keywords="streaming, sse, server-sent events, chunks"}

Add `"stream": true` and the same call is answered as `text/event-stream`: `chat.completion.chunk` events in arrival order, then the terminal `[DONE]` frame. Ask for the token counts with `stream_options` and a usage chunk arrives before that frame.

A refusal decided before the first event is answered as an ordinary JSON error, exactly as an unary call would be. A failure after the stream has started ends it **without** the terminal frame — which is how a client tells a finished answer from a truncated one, so treat a stream that stops before `[DONE]` as a failed call rather than a short one.

:::code-group
```bash title=curl
curl -N https://api.kumorouter.com/v1/chat/completions \
  -H "Authorization: Bearer $KUMO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "<model>",
    "max_tokens": 128,
    "stream": true,
    "stream_options": { "include_usage": true },
    "messages": [{ "role": "user", "content": "Write a haiku about latency." }]
  }'
```
```python title=Python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.kumorouter.com/v1",
    api_key=os.environ["KUMO_API_KEY"],
)

stream = client.chat.completions.create(
    model="<model>",
    max_tokens=128,
    stream=True,
    stream_options={"include_usage": True},
    messages=[{"role": "user", "content": "Write a haiku about latency."}],
)
for chunk in stream:
    for choice in chunk.choices:
        print(choice.delta.content or "", end="", flush=True)
```
```javascript title=Node
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.kumorouter.com/v1",
  apiKey: process.env.KUMO_API_KEY,
});

const stream = await client.chat.completions.create({
  model: "<model>",
  max_tokens: 128,
  stream: true,
  stream_options: { include_usage: true },
  messages: [{ role: "user", content: "Write a haiku about latency." }],
});

for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}
```
:::

> [Every operation, member by member →](page:api-reference) · [What a refusal looks like →](page:errors)

## Prove the key works {#verify-key keywords="verify, check, identity"}

Before you wire the key into anything, ask the platform what it thinks of it. The identity echo is the one operation that answers with the presenting key's own identity — its name, what funds it, when it expires — so it says "yes, this key is live, and here is what it may do" without you writing a line of code.

> [Check your key →](page:key-check)

---

<!-- https://documentation.kumorouter.com/authentication · Markdown: https://documentation.kumorouter.com/authentication.md -->

---
title: Authentication
description: 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.
keywords: authentication, bearer, authorization, api key, x-api-key, 401
group: get-started
---

## The header {#bearer-header keywords="bearer, authorization, header, token"}

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 it authenticates is spent and counted against that organization.

```bash title=Shell
# 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.
Authorization: Bearer $KUMO_API_KEY
```

> [The whole call, end to end →](page:quickstart)

## The Anthropic carrier {#anthropic-header keywords="x-api-key, anthropic, sdk, messages"}

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.

:::code-group
```bash title=curl
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." }]
  }'
```
```python title=Python
import os
from anthropic import Anthropic

client = Anthropic(
    base_url="https://api.kumorouter.com",
    api_key=os.environ["KUMO_API_KEY"],
)

answer = client.messages.create(
    model="<model>",
    max_tokens=128,
    messages=[{"role": "user", "content": "Explain tokens in one line."}],
)
print(answer.content[0].text)
```
:::

## What a key looks like {#key-shape keywords="prefix, kumo_sk, secret, mask, rotation"}

:::deflist
| Property | Value |
| --- | --- |
| Prefix | **kumo_sk_** |
| Shown | once, on the screen that creates it |
| Stored | as a hash, plus a prefix and a tail for display |
:::

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 — and 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, which 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 number is the token an edit compares against, so 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 fragments are what the identity echo answers with, so a screenshot of a key check leaks nothing.

> [Mint a key →](https://console.kumorouter.com/) · [Read a key back →](page:key-check)

## What a key may reach {#key-scope keywords="scope, modality, model, vendor, binding"}

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.

| member | what it is |
| --- | --- |
| `modality_codes` | Which kinds of work the key may ask for — text generation, embeddings, image generation. |
| `model_ids` | The exact models the key may name, when it is meant to be narrower than a modality. |
| `vendor_ids` | The suppliers behind those models, for a key tied to one of them. |
| `binding` | What pays for the key: the account wallet, or a prepaid package lane. It is chosen when the key is created and is immutable afterwards. |

A call that names something outside those lists is refused rather than routed: a scope is a whitelist, not a preference. The catalog of what there is to name at all lives on the [price list](https://kumorouter.com/pricing), and the gateway will recite the ids it carries to `https://api.kumorouter.com/v1/models`.

## One refusal, and one only {#one-refusal keywords="401, unauthorized, oracle, refusal"}

Every refusal of a presentation is the same 401. A missing header, a malformed key, an unknown key, a revoked key and an expired key are answered alike, and 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 →](page:key-check) · [Every status the gateway answers with →](page:errors)

---

<!-- https://documentation.kumorouter.com/key-check · Markdown: https://documentation.kumorouter.com/key-check.md -->

---
title: Check your key
description: The one live operation on this site: paste a key, and the platform reads back what it is and what it may do.
keywords: key check, verify, identity, echo, 401
group: get-started
---

## What it does {#what-it-does keywords="identity, echo, verify, live"}

Every model call through the gateway is authenticated by this same API key. The identity echo is the one that answers **about** the key instead of with a model's output: it reads the presenting key's own record back to you. That is exactly why it belongs on the documentation site — it is the shortest honest answer to "does my key work", and it is the one live tool this site runs for itself.

What comes back is the key's own record — the name you gave it, the masked prefix and tail that identify it, what funds it, when it expires if it ever does, the scope it may reach and the version it is on. The secret is not in that answer. One operation does carry it — the call that mints the key answers with it once, which is the only time it is ever shown — and nothing afterwards reads it back.

The key you type lives in one place — the field below — and leaves this page only as the bearer header of that one request. It is never written to storage, to a cookie, to the address bar or to a log, and it is dropped from the page the moment the answer arrives, whether the answer is yes or no. A second check is a second paste.

A refused key gets one sentence and not four. The platform answers an unknown, a revoked, an expired and a malformed key with the same status on purpose: telling them apart would turn the surface into an oracle over key material.

## Run it {#run-it keywords="paste, check, live"}

:::slot key-check
:::

> [How the header is written →](page:authentication) · [What every status means →](page:errors)

---

<!-- https://documentation.kumorouter.com/errors · Markdown: https://documentation.kumorouter.com/errors.md -->

---
title: Errors
description: The envelope a refusal arrives in, the status codes the gateway answers with, and the two behaviours a client has to be written around.
keywords: errors, status codes, 401, 429, envelope, streaming, failover
group: gateway
---

## The refusal envelope {#envelope keywords="error, envelope, type, message, param, json"}

A refused call answers with one JSON object carrying a single `error` member. It is this protocol's own envelope and not a translation of another's: the same shape refuses a Chat Completions call, a Responses call and a Messages call, and the Messages surface wraps it with the top-level `"type": "error"` its own clients look for.

Two members are always present — `type` and `message` — and two more appear when there is something to say.

| Member | What it carries |
| --- | --- |
| `type` | The class of failure, in this protocol's own closed vocabulary: invalid_request_error, not_found_error, authentication_error, permission_error, rate_limit_error, api_error, and on the Messages surface overloaded_error as well. |
| `message` | What went wrong, in a fixed safe sentence. It is never built from anything the request carried, so it can never quote a prompt, a header or a key back at a log. |
| `code` | The machine-readable reason. One reason has one spelling across every surface of this platform, which is what makes it the thing a client branches on. |
| `param` | The request member at fault, when one member is at fault. |

Branch on `type` and on `code`, never on the wording of `message`: the wording is chosen to be safe to print, not to be parsed.

```json title=Refusal
{
  "error": {
    "type": "authentication_error",
    "message": "The request is not authenticated."
  }
}
```

## Status codes {#error-codes keywords="400, 401, 403, 404, 429, 500, 503, status"}

| Status | What it means |
| --- | --- |
| `400` | The request is not valid — a malformed body, or a member this surface does not take. |
| `401` | The request is not authenticated. |
| `403` | The key may not call this model, or the organization is frozen. |
| `404` | No such model. |
| `429` | A rate or spending limit refused the request. |
| `500` | The request could not be completed. |
| `503` | No provider can currently serve this request. |

Two of them are worth telling apart before you write a retry. A `429` is a ceiling you are over, and time is what clears it; a `503` is that nothing upstream can serve the call at this moment. Neither is fixed by retrying at once, and a client that treats every refusal as "try again immediately" spends its whole allowance on being refused.

## One 401, whatever is wrong with the key {#key-refusals keywords="401, unauthorized, revoked, expired, unknown"}

A missing key, a malformed key, an unknown key, a revoked key and an expired key are answered alike — the same `401`, the same sentence — and that is a decision rather than an omission. Telling them apart would turn the gateway into an oracle over key material: whoever can see the difference between "there is no such key" and "that key was revoked" is reading the platform's key table one guess at a time.

So a `401` says exactly one thing: this call was not admitted. Check that the header is there and spelled as a bearer token, that the key is the whole secret you copied when you minted it, and that the key is still live. The key check answers all three at once, from a browser, without a line of code.

> [Check your key →](page:key-check) · [How the header is written →](page:authentication)

## A stream that stops early {#stream-failure keywords="streaming, sse, done, truncated, incomplete"}

A refusal decided before the first event is an ordinary JSON error with a status, exactly as an unary call would be — the envelope above, and nothing streamed. A failure after the stream has started cannot be that: the status line has already gone out. Such a stream simply ends, **without** its terminal `[DONE]` frame.

That absence is the whole signal, so read for it. A client that treats "the connection closed" as "the answer finished" will hand a truncated answer to whatever comes next, and nothing in the bytes it received says otherwise. Treat a stream that ends before `[DONE]` as a failed call rather than a short one.

The other two surfaces say the same thing in their own events: on the Messages surface the failure arrives as this protocol's error event and the stream then ends without `message_stop`, and on the Responses surface the terminal event is `response.failed`, carrying this envelope in place of the finished body.

## Failover {#failover keywords="failover, channel, upstream, provider, retry"}

A model is usually reachable through more than one channel. When the one serving a call fails before any of the answer has reached you, the call carries on over the next channel for the same model rather than coming back to you.

Three kinds of failure end the call instead of moving it. A failure after the first bytes have reached you, because you cannot be handed a second answer half way through one you are already reading. A call you cancelled, because you are no longer waiting for it. And a call whose outcome upstream is not known — retrying that one could bill you twice for work that in fact succeeded.

So a `503` is not a promise that every route was tried. It also arrives when no channel is eligible for what the call asked for, before anything upstream is contacted at all, and the number of channels a single call will try is bounded rather than exhaustive.

> [What bounds your throughput →](page:limits)

---

<!-- https://documentation.kumorouter.com/limits · Markdown: https://documentation.kumorouter.com/limits.md -->

---
title: Rate limits
description: The ceilings a call is measured against, what a call that reaches one gets back, and how a higher ceiling is asked for.
keywords: rate limits, throughput, requests per second, tokens per minute, 429, quota
group: gateway
---

## What a new key carries {#defaults keywords="default, requests per second, tokens per minute, quota"}

A key is issued with two ceilings, and a call has to be under both of them. Nothing is switched on to get them, nothing in a request asks for them, and a key that has just been minted already has them.

:::deflist
| Ceiling | Default |
| --- | --- |
| Requests | **20 per second** |
| Tokens | **500,000 per minute** |
:::

Your organization carries the same pair, and a call is measured against the organization's first and its key's second — an organization's default is **ten keys' worth**, so **200 requests per second** and **5,000,000 tokens per minute**. It is not a sum of what the keys are allowed: issuing an eleventh key does not raise it. So a call can be well under both of its own key's ceilings and still be refused for either of the organization's, on traffic other keys generated — a quiet key can be turned away for the organization's second as readily as for its minute.

The ceilings are counted separately and on different clocks — one over a second, the other over a minute — so a workload can sit comfortably inside the request ceiling and still be over the token one. A handful of very large calls every second is exactly the shape that does it, and it is the shape that surprises people, because the request count looks fine.

## When a ceiling is reached {#what-happens keywords="429, rate limit error, estimate, usage, retry"}

A call that would cross either ceiling is refused rather than queued: the status is `429`, and the envelope's `type` is `rate_limit_error`. The refusal is decided before the call reaches a provider, so nothing upstream sees it.

The request ceiling can be counted as calls arrive. The token ceiling cannot, because what a call will cost is not known until it has been answered — so the platform estimates the call's tokens before it goes upstream and counts the estimate against the minute, then reconciles that estimate against the usage the answer actually reports. Where that reconciliation lands, the minute carries the real figure rather than the guess, and a run of calls that came in far under their estimates leaves room behind it. Two cases end differently, and both are deliberate. The correction is aimed at the exact minute the call was charged to, so once a later call has carried a counter into a new minute there is nothing left to correct — that minute is not what the counter is on any more. Time passing does not do this; a further call does. And the two counters move on independently, so a correction can still land on one of them and find nothing on the other. The second case is the one where the estimate really does stand: an operation that reports no tokens at all keeps what it was charged: image generation is priced in images, not tokens, so correcting it to zero would refund the whole estimate and let that surface cost nothing against the ceiling.

Streaming does not change the arithmetic: a stream that has begun is a call that has been counted. Counting tokens does not enter it at all — that operation calls no provider and consumes no quota, which is what makes it safe to run in front of a large request.

Back off before retrying a `429`, lengthen the wait with every attempt, and cap the number of attempts.

> [What a refusal looks like →](page:errors)

## Asking for more {#raising keywords="raise, increase, support, ceiling"}

A raise is a conversation, not a setting. Neither ceiling is editable in the console, and no member of a request asks for a higher one: write to support with the traffic you need admitted — the shape of it, not only the peak — and the ceilings on your key are changed for you.

---

<!-- https://documentation.kumorouter.com/api-reference · Markdown: https://documentation.kumorouter.com/api-reference.md -->

---
title: API reference
description: Every operation of the Kumo gateway, generated from the published API description: the address, how it is authenticated, what the request carries and what comes back.
keywords: api, reference, endpoints, operations, openapi
group: gateway
generated: from the published API description this build was compiled against
---

This page is generated from the API description this build was compiled against, so it says what the gateway answers rather than what it was once documented to answer. Every member below is a member of the wire.

## POST /v1/chat/completions {#chat-completions-create keywords="Create a chat completion."}

:::deflist
| field | value |
| --- | --- |
| Method | **POST** |
| Path | /v1/chat/completions |
| Auth | Authorization: Bearer <key> |
| Operation | public.chat_completions.create |
:::

Create a chat completion.

Answers one Chat Completions request against a model of the published catalog, authenticated by a Kumo API key. It is this protocol served natively and not a translation of another: the request members, the response shape, the usage vocabulary and the error envelope are this protocol's own. A stream=true request is answered as text/event-stream — chat.completion.chunk events, a usage chunk when stream_options.include_usage asks for one, then the terminal [DONE] frame; a refusal decided before the first event is answered as this protocol's ordinary JSON error, and a failure after it ends the stream without its terminal frame. An output ceiling is required — max_tokens or max_completion_tokens — because a billable dimension with no finite ceiling cannot be reserved for before the upstream call.

The request carries:

| member | what it is |
| --- | --- |
| `max_completion_tokens` | integer, optional — The output ceiling, in the current spelling. |
| `max_tokens` | integer, optional — The output ceiling, in the spelling long-established clients send. |
| `messages` | array of ChatMessage, required — The conversation, in order. |
| `model` | string, required — The model to answer with: a canonical name or an alias the published catalog carries. |
| `response_format` | ChatResponseFormat, optional — A structured output requirement. |
| `stream` | boolean, optional — Whether to stream the answer. |
| `stream_options` | ChatStreamOptions, optional — Options that apply only when stream is true. |
| `tool_choice` | ChatToolChoice, optional — A requirement that one named declared tool be called on this turn. |
| `tools` | array of ChatTool, optional — The tools this turn may call. |

The answer:

| what | shape |
| --- | --- |
| `application/json` | ChatCompletionsReply |
| `text/event-stream` | ChatCompletionsChunk |
| `on failure` | ChatCompletionsError |

## POST /v1/embeddings {#embeddings-create keywords="Create embedding vectors for a batch of inputs."}

:::deflist
| field | value |
| --- | --- |
| Method | **POST** |
| Path | /v1/embeddings |
| Auth | Authorization: Bearer <key> |
| Operation | public.embeddings.create |
:::

Create embedding vectors for a batch of inputs.

Returns one vector per input, in the order the inputs were given. Billing counts input tokens only: this surface produces no output tokens and no image units, and the ledger records zero for both. The batch is bounded — at most 128 inputs and 131072 bytes in total — and a request outside those bounds is refused before any provider is called. Streaming is not part of this surface. `encoding_format` must be stated and must be "base64": vectors are returned as base64-encoded little-endian float32, and this surface does not serve this protocol's "float" default. A request asking for float, or asking for nothing and therefore for float, is refused by name rather than answered in another format.

The request carries:

| member | what it is |
| --- | --- |
| `encoding_format` | "base64", required — Must be "base64": this surface does not serve the protocol's "float" default. |
| `input` | array of string, required — The batch of inputs to embed, at most 128 members and 131072 bytes in total. |
| `model` | string, required — The catalog model to embed with, as the customer names it. |

The answer:

| what | shape |
| --- | --- |
| `application/json` | EmbeddingsResponseBody |
| `on failure` | EmbeddingsError |

## POST /v1/images/generations {#images-generate keywords="Generate images from a prompt."}

:::deflist
| field | value |
| --- | --- |
| Method | **POST** |
| Path | /v1/images/generations |
| Auth | Authorization: Bearer <key> |
| Operation | public.images.generate |
:::

Generate images from a prompt.

Generates one or more images from a text prompt with an image-capable model. The request and response envelopes are OpenAI-compatible, and so is the error envelope: refusals carry `error.message`, `error.type` and `error.code` rather than Kumo's REST envelope. Billing is per image unit, debited through the same admission, reservation and settlement kernel as every other model surface. Edits and variations are not served.

The request carries:

| member | what it is |
| --- | --- |
| `model` | string, required — The public model name to generate with. |
| `n` | integer, optional — How many images to generate. |
| `prompt` | string, required — The prompt to generate an image from. |
| `size` | string, optional — The image size as WIDTHxHEIGHT, for example 1024x1024. |

The answer:

| what | shape |
| --- | --- |
| `application/json` | ImagesResponseBody |
| `on failure` | ImagesErrorBody |

## POST /v1/messages {#anthropic-messages-create keywords="Create a message."}

:::deflist
| field | value |
| --- | --- |
| Method | **POST** |
| Path | /v1/messages |
| Auth | Authorization: Bearer <key> or x-api-key: <key> |
| Operation | public.anthropic_messages.create |
:::

Create a message.

Answers one Anthropic Messages request against a model of the published catalog, authenticated by a Kumo API key. It is this protocol served NATIVELY and not a translation of another: max_tokens is required and has no default, the system prompt is a member of the request rather than a turn of the conversation, tools carry input_schema with no function wrapper, the answer states stop_reason and content blocks, and refusals arrive in this protocol's own error envelope. A tool exchange is carried natively — an assistant's calls as tool_use blocks with input as the JSON object the tool schema describes, and their results as tool_result blocks of the following user turn. A stream=true request is answered as text/event-stream carrying this protocol's own named events — message_start, content_block_start, content_block_delta, content_block_stop, message_delta, message_stop — with a refusal decided before the first event answered as this protocol's ordinary JSON error, and a failure after it expressed in-stream as this protocol's error event, the stream then ending without message_stop. The credential may be presented as `Authorization: Bearer <key>` or, as the native API does it, bare in the `x-api-key` header; either admits the caller and both name the same Kumo key. Sampling is carried to the supplier unchanged: temperature, top_p and stop_sequences. Four members are ACCEPTED AND HAVE NO EFFECT, and each says so in its own description rather than being refused or silently honoured — metadata, which this platform does not forward because it would describe a customer's own user to a supplier, and thinking, output_config and context_management, which are beta configuration this platform proves on no tuple.

The request carries:

| member | what it is |
| --- | --- |
| `context_management` | object, optional — This protocol's context-management configuration, as the beta spells it. |
| `max_tokens` | integer, required — The maximum number of tokens to generate. |
| `messages` | array of MessagesInputMessage, required — The conversation, oldest turn first. |
| `metadata` | MessagesMetadata, optional — This protocol's request metadata. |
| `model` | string, required — The catalog model to answer with, as the customer names it. |
| `output_config` | object, optional — This protocol's output-effort configuration, as the beta spells it. |
| `stop_sequences` | array of string, optional — Sequences that end the answer when the model produces one. |
| `stream` | boolean, optional — Stream the answer as this protocol's own named events over text/event-stream: message_start, content_block_start, content_block_delta, content_block_stop, message_delta, message_stop. |
| `system` | string or array of MessagesSystemBlock, optional — The system prompt, beside the conversation rather than as a turn of it. |
| `temperature` | number, optional — How much randomness to use, from 0 to 1. |
| `thinking` | object, optional — This protocol's extended-thinking configuration, as the beta spells it. |
| `tool_choice` | MessagesToolChoice, optional — Forces one of the declared tools. |
| `tools` | array of MessagesTool, optional — The tools this turn may call. |
| `top_p` | number, optional — Nucleus sampling, from 0 to 1. |

The answer:

| what | shape |
| --- | --- |
| `application/json` | MessagesReply |
| `text/event-stream` | MessagesStreamEvent |
| `on failure` | MessagesError |

## POST /v1/messages/count_tokens {#anthropic-messages-count-tokens keywords="Count message input tokens."}

:::deflist
| field | value |
| --- | --- |
| Method | **POST** |
| Path | /v1/messages/count_tokens |
| Auth | Authorization: Bearer <key> or x-api-key: <key> |
| Operation | public.anthropic_messages.count_tokens |
:::

Count message input tokens.

Authenticates a Kumo API key and returns a deterministic local estimate for the same native Messages input accepted by /v1/messages. It performs no provider call, creates no customer request or reservation, touches no balance, and consumes no customer RPS/RPM quota. max_tokens and stream are generation-only and are refused on this operation. The credential may be presented as `Authorization: Bearer <key>` or, as the native API does it, bare in the `x-api-key` header; either admits the caller and both name the same Kumo key.

The request carries:

| member | what it is |
| --- | --- |
| `context_management` | object, optional — This protocol's context-management configuration, as the beta spells it. |
| `messages` | array of MessagesInputMessage, required — The conversation, oldest turn first. |
| `metadata` | MessagesMetadata, optional — This protocol's request metadata. |
| `model` | string, required — The catalog model to answer with, as the customer names it. |
| `system` | string or array of MessagesSystemBlock, optional — The system prompt, beside the conversation rather than as a turn of it. |
| `thinking` | object, optional — This protocol's extended-thinking configuration, as the beta spells it. |
| `tool_choice` | MessagesToolChoice, optional — Forces one of the declared tools. |
| `tools` | array of MessagesTool, optional — The tools this turn may call. |

The answer:

| what | shape |
| --- | --- |
| `application/json` | AnthropicTokenCountReply |
| `on failure` | MessagesError |

## GET /v1/models {#models-list keywords="List enabled, evidence-backed models."}

:::deflist
| field | value |
| --- | --- |
| Method | **GET** |
| Path | /v1/models |
| Auth | None — this operation is open. |
| Operation | public.models.list |
:::

List enabled, evidence-backed models.

Returns only active catalog models that have at least one enabled capability in the currently published provider configuration. Protocol and modality support are the exact intersection of catalog declarations and routable provider evidence; disabled or unproven tuples are absent.

The request carries no body.

The answer:

| what | shape |
| --- | --- |
| `application/json` | PublicModelCatalog |
| `on failure` | ErrorEnvelope |

## POST /v1/responses {#responses-create keywords="Create a response."}

:::deflist
| field | value |
| --- | --- |
| Method | **POST** |
| Path | /v1/responses |
| Auth | Authorization: Bearer <key> |
| Operation | public.responses.create |
:::

Create a response.

Answers one Responses request against a model of the published catalog, authenticated by a Kumo API key. It is this protocol served natively and not a translation of another: the input items, the output items, the usage vocabulary and the error envelope are this protocol's own. A stream=true request is answered as text/event-stream in this protocol's own named events — response.created, the output items and their deltas, then exactly one terminal event: response.completed carrying the finished body and usage, or response.failed carrying this protocol's error envelope. A failure before the first event is answered as this protocol's ordinary JSON error; cancellation releases what the request held. The output ceiling — max_output_tokens — is optional, and a request stating none is answered under this surface's published default of 32768 tokens; this platform requires a FINITE bound before the upstream call, and a published default is one the caller can read in advance. An explicit zero is refused: it is a request for no output at all. instructions is implemented and becomes the conversation's leading system turn; a developer turn is carried as a system turn, the two being one role under two names. store is accepted only as false, because this platform retains no response and accepting true would promise a retrieval that cannot happen. Six members are ACCEPTED AND HAVE NO EFFECT, each saying so in its own description rather than being refused or silently honoured — parallel_tool_calls, reasoning, include, prompt_cache_key, client_metadata and text.verbosity. A previous_response_id is validated for OWNERSHIP and is not honoured as server-side context: this platform persists no prompt, response or chunk, so there is nothing to continue from and the caller sends its own context.

The request carries:

| member | what it is |
| --- | --- |
| `client_metadata` | object, optional — Metadata the client keeps about its own session. |
| `include` | array of string, optional — Extra members the caller asks the answer to carry. |
| `input` | array of ResponsesInputItem, required — The conversation, in order, as typed items. |
| `instructions` | string, optional — The system prompt, as this protocol carries it: a member of the request rather than a turn of the conversation. |
| `max_output_tokens` | integer, optional — The output ceiling. |
| `model` | string, required — The model to answer with: a canonical name or an alias the published catalog carries. |
| `parallel_tool_calls` | boolean, optional — Whether the model may make several tool calls in one turn. |
| `previous_response_id` | string, optional — A response of this organization that this request follows. |
| `prompt_cache_key` | string, optional — An opaque key the caller uses to group requests for prompt caching. |
| `reasoning` | object, optional — This protocol's reasoning configuration. |
| `store` | boolean, optional — Whether the supplier should retain this response for later retrieval. |
| `stream` | boolean, optional — Whether to stream the answer. |
| `text` | ResponsesTextConfig, optional — How the answer's text is shaped. |
| `tool_choice` | ResponsesToolChoice, optional — A requirement that one named declared tool be called on this turn. |
| `tools` | array of ResponsesTool, optional — The tools this turn may call. |

The answer:

| what | shape |
| --- | --- |
| `application/json` | ResponsesReply |
| `text/event-stream` | ResponsesStreamEvent |
| `on failure` | ResponsesError |

> Every operation above is emitted from the API description this build was compiled against. The whole of this documentation as one Markdown document is described on the [machine-readable page](page:machine-readable).

---

<!-- https://documentation.kumorouter.com/machine-readable · Markdown: https://documentation.kumorouter.com/machine-readable.md -->

---
title: Machine-readable documentation
description: This documentation as Markdown — an index, the whole corpus in one document, and every page addressable as its own source.
keywords: llms.txt, llms-full.txt, markdown, agent, machine-readable, prompt
group: resources
---

## The two documents {#llms-txt keywords="llms.txt, llms-full.txt, markdown, agent, index"}

Running an agent? This site publishes itself. [/llms.txt](https://documentation.kumorouter.com/llms.txt) is the index of this documentation in the llms.txt convention — one top-level title, a one-line summary, then the pages — which is a shape any agent can rely on without being taught this site in particular.

[/llms-full.txt](https://documentation.kumorouter.com/llms-full.txt) is the other half of the same idea: the entire public documentation corpus as one Markdown document, every page in the order the site lists them. It is what to fetch when the agent is going to answer questions rather than look one thing up, and it costs a fraction of the tokens that reading the rendered HTML would.

Both are assembled from the same pages the site itself renders from, so neither can drift into being a stale second copy of the product — the index from the title and summary each page declares, the full document from the whole of their text, both in the build that changes the pages themselves.

## Any page as Markdown {#raw-markdown keywords="raw, .md, source, copy, clipboard"}

Every page of this site is also addressable as its own Markdown source: append `.md` to the page's path and the answer is the Markdown this page was rendered from, with this deployment's addresses already filled into it — the quickstart, for example, is served as Markdown at its own address with `.md` on the end, and the hostnames in its examples are the ones you would actually call. Links from one page of this site to another stay in the site's own `page:` form rather than becoming URLs, so a fetcher can tell an internal reference from an address it should follow. The home page is the one address the rule does not fit — it is served at `/index.md`, because `/.md` is not a name. That is the one page to fetch when an agent needs a single topic and not the whole corpus.

The article header carries a copy-as-Markdown control, which puts exactly that document on the clipboard — the same text the address serves, for pasting into a conversation rather than fetching over the network.

## Point an agent at it {#agent-prompt keywords="prompt, agent, assistant, instruction"}

:::prompt
Hand this to a coding assistant before you ask it anything about Kumo. It is English-only by design — that is how coding assistants follow an instruction most reliably.

```text
Read https://documentation.kumorouter.com/llms-full.txt before answering anything about Kumo.

It is the whole of this documentation as one Markdown document: what the product
is, how a key is issued, how a request is shaped, what comes back, and what a
refusal looks like. Prefer it over crawling the rendered site — it is the same
information at a fraction of the tokens, and it is generated from the site's own
pages rather than written twice.

Any single page is also available as Markdown: take the page's address, drop any
#fragment from it, and add .md. The home page is the exception and is served at
/index.md.

These documents name no model id and no price: they carry a <model> placeholder
wherever one would go. Read the live catalogue and pricing endpoints they point to
for those values, and do not invent one.
```
:::

> [Every operation, member by member →](page:api-reference)
