API documentation
Compatible with the OpenAI client libraries, and with the Claude CLI.
Quickstart
The shortest path needs no code at all. If you use the Claude CLI, point two environment variables at us and run it — there is nothing to install, no wrapper to adopt, and no model to configure.
# Claude Code appends /v1/messages itself — no /v1 on this line.
export ANTHROPIC_BASE_URL="https://api.claudcli.com"
export ANTHROPIC_AUTH_TOKEN="sk-your-key-here"
claude
On Windows the same two lines are set rather than
export, and they belong in the window you run the CLI from:
set ANTHROPIC_BASE_URL=https://api.claudcli.com
set ANTHROPIC_AUTH_TOKEN=sk-your-key-here
claude
A new window inherits whatever the machine already has set. If
ANTHROPIC_BASE_URL or ANTHROPIC_API_KEY is
defined for the whole machine — some tools set it for you — then a
window where you did not type the lines above will use those values
instead, and nothing about the CLI's output says so. Type them in every
window, and run set ANTHROPIC_ when a run seems to be
answering from somewhere else.
That is the whole setup. Left to itself the CLI asks for the model it ships with; we serve the equivalent tier, and go on serving it when a new version of that model is released. Choosing a model yourself is optional:
# Optional — pick the tier instead of letting the CLI pick it.
export ANTHROPIC_MODEL="Haiku 4.5"
export ANTHROPIC_SMALL_FAST_MODEL="Haiku 4.5"
The mistake that is easy to make here is documented under
Claude Code: the base URL takes
no /v1.
Or, with the OpenAI SDK
Any OpenAI-compatible client works too, and there is no separate
library to learn. The only differences from the OpenAI defaults are
base_url and api_key.
pip install openai
from openai import OpenAI
client = OpenAI(
base_url="https://api.claudcli.com/v1",
api_key="sk-your-key-here",
)
response = client.chat.completions.create(
model="Haiku 4.5",
messages=[{"role": "user", "content": "Explain vector databases briefly."}],
)
print(response.choices[0].message.content)
Authentication
Every request carries your API key as a bearer token. Keys are created in the dashboard and shown once, at the moment they are created — we store only a hash, so a lost key is replaced rather than recovered.
Authorization: Bearer sk-your-key-here
A key that has been revoked, an account that has been suspended, and a key that never existed all return the same response. That is deliberate: it means a key you have cancelled tells an attacker nothing about whether it was ever real.
Chat completions
/v1/chat/completions
The request body follows the OpenAI schema. These fields are supported:
| Field | Type | Notes |
|---|---|---|
| model | string | Required. One of the model ids below. |
| messages | array | Required. The conversation so far. |
| max_tokens | integer | Clamped to the model's ceiling and to your remaining balance. |
| temperature | number | |
| top_p | number | |
| stop | string or array | |
| stream | boolean | See Streaming. |
| tools | array | Tool calling is supported. |
| tool_choice | string or object | |
| response_format | object | JSON mode where the model supports it. |
| seed | integer |
Unknown fields are dropped rather than forwarded. If a parameter you rely on is not listed above, send us the request shape and we will add it — but it will not silently reach the model in the meantime.
Streaming
Set stream: true and consume the response as server-sent
events, exactly as with the OpenAI SDK. The final frame is
data: [DONE].
stream = client.chat.completions.create(
model="Haiku 4.5",
messages=[{"role": "user", "content": "Count to ten."}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content if chunk.choices else None
if delta:
print(delta, end="", flush=True)
[DONE] carries the token counts and has an
empty choices array. If your loop indexes
choices[0] without checking, that frame will raise — this
is standard OpenAI behaviour and the snippet above guards for it.
Long silences are kept alive with SSE comment frames. Every client library ignores them, and they exist so an idle connection is not closed by a proxy while the model is still thinking.
Claude Code
/v1/messages
The Claude CLI does not speak the OpenAI format, and it has no setting to make it. So the same models are also served over the Anthropic Messages API at the path above, and the CLI works against this service unmodified. Requests are translated on the way out and back, so everything else on this page — token accounting, streaming, tool calling, the balance — behaves identically on either endpoint.
The setup is in the Quickstart; what follows is what each variable does, and which of them you can leave out.
| Variable | Notes |
|---|---|
| ANTHROPIC_BASE_URL |
The API host, with no /v1 — the
CLI adds the path itself, so a version prefix here produces
/v1/v1/messages. This is the one people get wrong,
because every other snippet on this page does take the prefix.
A doubled prefix is absorbed rather than refused, so it will
still work — set it correctly anyway, because it is the request
the CLI means to send.
|
| ANTHROPIC_AUTH_TOKEN |
Your sk- key. ANTHROPIC_API_KEY works
too and is sent as a different header; both carry the same
credential.
|
| ANTHROPIC_MODEL | Optional. Pins the model the CLI reasons with to one of the ids under Models. Left unset, the CLI asks for the model it ships with and we serve the tier that belongs to. |
| ANTHROPIC_SMALL_FAST_MODEL | Optional. The model for background work — summaries, titles and similar. Left unset, the CLI asks for its own small model, which is served here too. Setting it is a choice, not a requirement. |
| CLAUDE_CODE_MAX_CONTEXT_TOKENS | Optional, and worth setting on a large codebase. The CLI decides how much context it has from its own model catalogue and not from ours, so it can assume far less room than the model you are actually talking to. Set it to the context window published under Models to use all of it. |
Leaving the model unset
With neither model variable set, the CLI sends its own default ids. Those are served here, and each is matched by family — every Opus id is the Opus tier, every Haiku id the Haiku tier — so a version released after this page was written resolves without anyone having to add it first.
Those ids are not the ones under Models. The
catalogue publishes our own names, deliberately; both resolve, and a
family id is answered at whichever tier that family names. Set
ANTHROPIC_MODEL to a catalogue id when you would rather be
explicit about which tier you are buying.
What the CLI sends is translated into the same request as
/v1/chat/completions, so a session is billed, limited and
metered exactly as any other request. Errors come back in the Anthropic
envelope — {"type": "error", "error": {...}} — while the
status codes and the codes listed under
Errors are the same on both.
Models
/v1/models
/v1/models/{model_id}
| Model id | Name | Context tokens | Max output tokens |
|---|---|---|---|
| Haiku 4.5 | Haiku 4.5 | 200,000 | 64,000 |
| Opus 5 | Opus 5 | 1,000,000 | 128,000 |
| Sonnet 5 | Sonnet 5 | 1,000,000 | 128,000 |
Tokens and billing
Your balance is a number of tokens. It moves in three steps, and all three are visible in the dashboard:
-
Reserve. Before your request is sent, we hold an
upper bound on what it could cost, based on the prompt length and
max_tokens. If the hold does not fit in your balance, the request does not start and nothing is sent. - Serve. The request runs. You are not charged for the hold — it is a hold, not a charge.
- Settle. When the response finishes, the hold is replaced by the token count the model reported. The difference is returned to your balance immediately.
This is why a request can stop on balance while you still have tokens: the hold is an upper bound on the whole request, so a prompt larger than your remaining balance does not start, rather than failing part-way through. Where your balance covers the prompt but not the full output you asked for, the output is trimmed to what it can afford and you get a shorter answer instead.
Charging is based on the counts the model reports, not on our own estimate. If a stream ends before those counts arrive — because the connection dropped — you are charged for the prompt plus what was actually delivered, never for the full hold. Each ledger row says which of the two it was.
Errors
Errors use the OpenAI envelope, so existing error handling keeps working.
{
"error": {
"message": "Incorrect API key provided.",
"type": "invalid_request_error",
"param": null,
"code": "invalid_api_key"
}
}
On /v1/messages the same errors
come back in the Anthropic envelope instead, which is what the CLI
expects. The status codes below and the codes in the table are
identical on both; only the wrapper differs.
{
"type": "error",
"error": {
"type": "invalid_request_error",
"message": "Incorrect API key provided."
}
}
| Status | Meaning | What to do |
|---|---|---|
| 400 | The request body was malformed. | Check the field named in param. |
| 401 | The API key was not accepted. | Check the key. Do not retry — this will not fix itself. |
| 402 | Your token balance cannot cover this request. | Add tokens, then send the request again — a retry loop will not help. |
| 429 | Rate limit or concurrency limit reached. | Wait for the interval in Retry-After. |
| 502 | The model returned an error. | Retry with backoff. |
| 503 | The service is temporarily unavailable. | Retry with backoff. We are already looking at it. |
| 504 | The request timed out. | Retry, or lower max_tokens. |
A failed request is not billed. If the model rejects the call, or the service is unavailable, your balance is untouched.
Limits
| Limit | Default | Notes |
|---|---|---|
| Requests per minute | Unlimited | Per API key. We will set one for you on request. |
| Concurrent requests | Unlimited | Per API key. Streaming requests hold their slot until they finish. |
| Maximum request body | 8 MiB | Enforced while reading, not after. |
There is no request rate limit and no concurrency limit by default. A request is stopped by your balance and by nothing else. Your own limits are shown on the dashboard, which is the authoritative number if it differs from the defaults here.