Docs · API
Calling Selan from your own code
One endpoint, one credential. The gateway speaks the Anthropic Messages API, so an existing integration moves across by changing a base URL and a token. For running Claude Code itself, start at the docs.
Get a token
An owner mints one in Selan under Settings → CLI tokens. It looks like this, and it is shown once:
export SELAN_TOKEN=selanct_...
A token spends as itself, under an address of its own like
billing-sync@tokens.selan.ai. Use one instead of a person's login:
what your service costs shows up as your service in Usage and in Logs, not
against whoever set it up. Give each caller its own token and name it after the
caller.
Revoking is immediate. Anything calling with a revoked token stops on its next request. There is no grace period and no draining, so rotate by minting the new one first.
Make a request
POST /v1/messages against the gateway. The body is the one you would
send to Anthropic directly. The
Messages API reference
documents every field, and none of them mean anything different here:
curl https://clgw.selan.ai/v1/messages \
-H "Authorization: Bearer $SELAN_TOKEN" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-H "x-selan-repo: gitlab.com/selan-ai/billing-sync" \
-d '{
"model": "claude-opus-5",
"max_tokens": 1024,
"messages": [{ "role": "user", "content": "Hello" }]
}'
No Anthropic key anywhere in that command. The gateway authenticates you, leases one of your company's connected credentials for the length of the request, streams the answer straight through, and releases it afterwards.
Streaming works the same way. Send "stream": true and the
server-sent events arrive as the provider emits them, forwarded and never
buffered.
From an SDK
The official Anthropic SDKs take both values in the constructor. Two options, and nothing else about your code changes:
import Anthropic from "@anthropic-ai/sdk"
const client = new Anthropic({
baseURL: "https://clgw.selan.ai",
authToken: "selanct_...",
defaultHeaders: {
"x-selan-repo": "gitlab.com/selan-ai/billing-sync",
},
})
const message = await client.messages.create({
model: "claude-opus-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello" }],
})
Python is the same two options, spelled its way:
from anthropic import Anthropic
client = Anthropic(
base_url="https://clgw.selan.ai",
auth_token="selanct_...",
default_headers={
"x-selan-repo": "gitlab.com/selan-ai/billing-sync",
},
)
message = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}],
)
Load the token from wherever you keep secrets; do not commit it. It is shown as a literal here only to be unambiguous about which option it goes in.
It has to be authToken, not apiKey. An
API key makes the SDK authenticate with an x-api-key header, and the
gateway reads only Authorization: Bearer. It strips
x-api-key before forwarding, so a client that authenticates that way
gets 401 on every request while the token itself is perfectly
good. Setting both options in code, as above, is also what stops a stray
ANTHROPIC_API_KEY in the environment from deciding this for you.
Attribute requests to a repository
Custom integrations can attach usage to the repository they are working on with
x-selan-repo. Send a host-qualified slug such as
gitlab.com/selan-ai/billing-sync: no scheme, credentials, leading
slash, or final .git. The value must be between 1 and 200 UTF-8
bytes. If the repository is unknown, omit the header instead of inventing or
sending an empty value.
Selan consumes this header for Usage and Insights; it is never forwarded to the
model provider. The repository is recorded only when an owner has enabled
Repository attribution for the company. The Selan CLI and
agent-run derive the slug automatically. Custom Claude Code Agent
SDK launches can send the same metadata through
ANTHROPIC_CUSTOM_HEADERS:
function runRepositoryTask(authToken: string, repository: string) {
return query({
prompt: "Review this repository",
options: {
env: {
...process.env,
ANTHROPIC_BASE_URL: "https://clgw.selan.ai",
ANTHROPIC_AUTH_TOKEN: authToken,
ANTHROPIC_CUSTOM_HEADERS: `x-selan-repo: ${repository}`,
},
},
})
}
Naming a model
A bare id like claude-opus-5 runs on your company's Anthropic
accounts. For anything else, name a provider and one of that provider's own model
ids. Nothing has to be added to Selan first:
{ "model": "openrouter:deepseek/deepseek-v4-flash-0731" }
{ "model": "openrouter:moonshotai/kimi-k3" }
{ "model": "openai:gpt-5.6-sol" }
The part before the first colon is the provider: openrouter,
openai, nexos or ollama. Everything after it
is that provider's own model id, passed through as-is, so any model they serve
works. Capitals, dashes and dots do not change which model you get.
Providers and models lists what each one can serve.
A DeepSeek request in full is one field different from the request above:
curl https://clgw.selan.ai/v1/messages \
-H "Authorization: Bearer $SELAN_TOKEN" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "openrouter:deepseek/deepseek-v4-flash-0731",
"max_tokens": 1024,
"messages": [{ "role": "user", "content": "Hello" }]
}'
Which provider keys your company has connected decides what you can reach. A
provider it has not connected, or a model that provider does not serve, is a
403, never a quiet fallback onto something cheaper or nearer. A
silent substitution would be a bill nobody authorised, and an answer from a model
you did not evaluate.
When it fails
Every error the gateway mints says whose fault it was, in the body and in the
headers: x-selan-source, x-selan-fault and
x-selan-request-id, mirroring
{ error, source, fault, request_id }. Quote the request id and we can
find the request.
The selan: prefix on the message is the cue for a client that reads
only the sentence: ours are prefixed, a provider's never is. A response carrying
x-selan-source: upstream and no fault is the provider's own (their
status, their headers, their bytes, relayed untouched), so handle those exactly as
you would today.
| Status and fault | What it means |
|---|---|
401 no_session |
No bearer, or none matching a live token. Check for a stray ANTHROPIC_API_KEY before assuming the token is dead. |
403 model_not_allowed |
Your company has not connected that provider, or it does not serve that model. |
402 over_budget |
A spend limit stopped the request. retry-after is honest: it is the moment the window actually resets. |
429 user_cap |
This caller's own concurrency cap never cleared. Retry, with less in flight. |
503 no_capacity |
Every credential in the company's pool is busy. Retry. |
| theirs, no fault | The provider answered. A 429 or 529 here is theirs, and means what it always did. |
What your company sees
Every request lands in Usage and in Logs under the token's own address, with the
model, the token count and the cost. That metering is automatic. Repository
attribution is optional metadata: custom clients send x-selan-repo,
and the Selan CLI derives it automatically.
If an owner has switched secret scanning on, request bodies are
scanned before they leave and any credential found is replaced with
[REDACTED: rule-id]. The request still goes through. See
Secret scanning.