selan.ai

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 faultWhat 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.