Developer documentation

Integrate the gateway

A drop-in, Anthropic-compatible API. If your code already talks to api.anthropic.com, changing the base URL and the API key is usually the only edit required. Every example below is copy-ready, paste it into a terminal and change only the key.

Base URL & authFirst request (curl)SDKs (TS & Python)Tool useStreamingImages & documentsRequest referenceErrors & retriesDifferencesOther endpointsResponse headersAvailable models

Base URL & authentication

Base URLhttps://private-claude-compatible-ai-gateway.halasai.com
Auth header (either form works)x-api-key: <your gateway key>Authorization: Bearer <your gateway key>

Where to get a key: keys are issued per application by an operator from the admin console (Applications → create a key). The full key is shown once at creation, store it securely, it looks like sk_accounting_4f9a2b6c8d1e3f50_9c1ae7b6d5f4c3b2a190837465f0e1d2c3b4a5968778695a4b3c2d1e0f1a2b3c, the fixed sk_ marker, then the owning application's slug, a public key id, and a secret. Only the non-secret prefix, sk_accounting_4f9a2b6c8d1e3f50, is retained and shown afterwards; the secret portion is never displayed again. Send the full key as the x-api-key header, or equivalently as Authorization: Bearer. Also send anthropic-version: 2023-06-01 for compatibility. Every response includes an x-request-id header, quote it when reporting an issue (see Response headers below).

Your first request (curl)

A non-streaming completion against the Messages endpoint:

curl https://private-claude-compatible-ai-gateway.halasai.com/v1/messages \
  -H "x-api-key: $GATEWAY_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "sonnet",
    "max_tokens": 512,
    "messages": [
      { "role": "user", "content": "Summarize the CAP theorem in two lines." }
    ]
  }'

Anthropic SDK

Point the official Anthropic SDK at the gateway with its base-URL option. Nothing else in your call changes.

TypeScript (@anthropic-ai/sdk)
import Anthropic from "@anthropic-ai/sdk";

// Point the official Anthropic SDK at your gateway. Only the baseURL and key
// change, every request shape is identical to api.anthropic.com.
const client = new Anthropic({
  baseURL: "https://private-claude-compatible-ai-gateway.halasai.com",
  apiKey: process.env.GATEWAY_API_KEY, // sent as x-api-key
});

const message = await client.messages.create({
  model: "sonnet",
  max_tokens: 512,
  messages: [{ role: "user", content: "Hello, Claude" }],
});

console.log(message.content);
Python (anthropic)
from anthropic import Anthropic

# Point the official Anthropic SDK at your gateway. Only base_url and
# api_key change, every request shape is identical to api.anthropic.com.
client = Anthropic(
    base_url="https://private-claude-compatible-ai-gateway.halasai.com",
    api_key=os.environ["GATEWAY_API_KEY"],  # sent as x-api-key
)

message = client.messages.create(
    model="sonnet",
    max_tokens=512,
    messages=[{"role": "user", "content": "Hello, Claude"}],
)

print(message.content)

Tool use, the full round trip

Your client executes tools, the gateway never does. The gateway forwards tool definitions to the model and streams back its tool_use request; running the function and returning a tool_result is entirely your responsibility.

The four steps, end to end:

Step 1, request with tools
// 1. You send the request WITH tool definitions. The gateway forwards them
//    to the model; it never executes a tool itself.
{
  "model": "sonnet",
  "max_tokens": 1024,
  "tools": [
    {
      "name": "get_weather",
      "description": "Get the current weather for a city.",
      "input_schema": {
        "type": "object",
        "properties": { "city": { "type": "string" } },
        "required": ["city"]
      }
    }
  ],
  "messages": [
    { "role": "user", "content": "What's the weather in Riyadh?" }
  ]
}
Step 2, model returns tool_use
// 2. The model replies asking to call the tool. stop_reason is "tool_use".
//    Note the tool_use id, you must echo it back.
{
  "id": "msg_01AbC...",
  "role": "assistant",
  "stop_reason": "tool_use",
  "content": [
    { "type": "text", "text": "Let me check that." },
    {
      "type": "tool_use",
      "id": "toolu_01Xy...",
      "name": "get_weather",
      "input": { "city": "Riyadh" }
    }
  ]
}
Step 3, you reply with tool_result (matching tool_use_id)
// 3. YOUR CODE runs get_weather("Riyadh"), then sends the result back as a
//    tool_result whose tool_use_id MATCHES the id from step 2. Replay the prior
//    turns so the model has the full context.
{
  "model": "sonnet",
  "max_tokens": 1024,
  "tools": [ /* same tool definitions as step 1 */ ],
  "messages": [
    { "role": "user", "content": "What's the weather in Riyadh?" },
    {
      "role": "assistant",
      "content": [
        { "type": "text", "text": "Let me check that." },
        { "type": "tool_use", "id": "toolu_01Xy...", "name": "get_weather", "input": { "city": "Riyadh" } }
      ]
    },
    {
      "role": "user",
      "content": [
        {
          "type": "tool_result",
          "tool_use_id": "toolu_01Xy...",
          "content": "38°C, clear sky"
        }
      ]
    }
  ]
}
Step 4, final answer
// 4. With the tool_result in hand, the model returns the final answer.
{
  "id": "msg_01Def...",
  "role": "assistant",
  "stop_reason": "end_turn",
  "content": [
    { "type": "text", "text": "It's 38°C and clear in Riyadh right now." }
  ]
}

Streaming (SSE)

Set "stream": true. The response is text/event-stream and follows Anthropic's event lifecycle; bytes are flushed as the runtime produces them.

curl -N https://private-claude-compatible-ai-gateway.halasai.com/v1/messages \
  -H "x-api-key: $GATEWAY_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "sonnet",
    "max_tokens": 512,
    "stream": true,
    "messages": [{ "role": "user", "content": "Write a haiku about caching." }]
  }'

# Response is text/event-stream, flushed token-by-token:
#   event: message_start        -> { id, model, role, usage }
#   event: content_block_start
#   event: content_block_delta   -> { delta: { type: "text_delta", text: "..." } }
#   event: content_block_stop
#   event: message_delta         -> { delta: { stop_reason }, usage: { output_tokens } }
#   event: message_stop

Images & documents

  • •Image and document blocks are accepted only in the FINAL message of a request. The gateway is stateless — it re-serializes the whole conversation on every call — so a media block sitting in an earlier turn has no channel to reach the model; the request is rejected with 400 invalid_request_error, never silently dropped.
  • •For a follow-up question about an image, re-attach the image to the new final turn. The gateway does not retain images across requests, so replaying only the earlier text turn (without the image bytes) means the model never sees the image again.
  • •Per-request limits are operator-set from the admin console — the defaults are 8 images per request and 5 MB per image. Exceeding either returns 400, as does a media block outside the final message.
  • •The gateway never silently drops a content block: a media block you send is either delivered to the model or the request is rejected outright.

A base64 image block alongside a text block, both in the final message:

curl https://private-claude-compatible-ai-gateway.halasai.com/v1/messages \
  -H "x-api-key: $GATEWAY_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "sonnet",
    "max_tokens": 512,
    "messages": [
      {
        "role": "user",
        "content": [
          {
            "type": "image",
            "source": {
              "type": "base64",
              "media_type": "image/png",
              "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
            }
          },
          { "type": "text", "text": "What color is this image?" }
        ]
      }
    ]
  }'

Request & content-block reference

Every field POST /v1/messages accepts:

modelRequired
string

Model name or alias; see the live list below. An unmapped name returns not_found_error.

messagesRequired
MessageParam[]

Conversation turns: { role: 'user' | 'assistant', content: string | ContentBlock[] }. Must be non-empty.

max_tokensRequired
integer > 0

Upper bound on generated tokens for this response.

systemOptional
string | TextBlock[]

System prompt, as a bare string or an array of text blocks.

streamOptional
boolean

Set true for an SSE (text/event-stream) response.

toolsOptional
Tool[]

Tool definitions forwarded to the model verbatim; the gateway never executes a tool.

tool_choiceOptional
'auto' | 'any' | 'tool' | 'none'

Controls whether, and which, tool the model must call. 'tool' requires naming a specific tool.

temperatureOptional
number, 0-1

Sampling temperature.

top_pOptional
number, 0-1

Nucleus sampling threshold.

top_kOptional
integer >= 0

Top-k sampling cutoff.

stop_sequencesOptional
string[]

Sequences that stop generation when produced.

metadataOptional
{ user_id?: string }

Optional pseudonymous per-user id for attribution — never a name or an email. See below.

thinkingOptional
{ type: 'adaptive' | 'enabled' | 'disabled', ... }

Extended-thinking configuration; enabled requires a budget_tokens.

FieldTypeRequiredNotes
modelstringRequiredModel name or alias; see the live list below. An unmapped name returns not_found_error.
messagesMessageParam[]RequiredConversation turns: { role: 'user' | 'assistant', content: string | ContentBlock[] }. Must be non-empty.
max_tokensinteger > 0RequiredUpper bound on generated tokens for this response.
systemstring | TextBlock[]OptionalSystem prompt, as a bare string or an array of text blocks.
streambooleanOptionalSet true for an SSE (text/event-stream) response.
toolsTool[]OptionalTool definitions forwarded to the model verbatim; the gateway never executes a tool.
tool_choice'auto' | 'any' | 'tool' | 'none'OptionalControls whether, and which, tool the model must call. 'tool' requires naming a specific tool.
temperaturenumber, 0-1OptionalSampling temperature.
top_pnumber, 0-1OptionalNucleus sampling threshold.
top_kinteger >= 0OptionalTop-k sampling cutoff.
stop_sequencesstring[]OptionalSequences that stop generation when produced.
metadata{ user_id?: string }OptionalOptional pseudonymous per-user id for attribution — never a name or an email. See below.
thinking{ type: 'adaptive' | 'enabled' | 'disabled', ... }OptionalExtended-thinking configuration; enabled requires a budget_tokens.

metadata.user_id lets the gateway record a pseudonymous per-user identifier alongside a request for attribution (e.g. per-user usage in the admin console). It must be an OPAQUE identifier you control — a database id or a hash — never a real name or an email address.

Content-block types a message's content array may contain:

text

Plain text content.

image

source: base64 (image/jpeg, image/png, image/gif, image/webp) or a url. Final message only — see Images & documents above.

document

source: base64 PDF, plain text, or a url; optional title, context, citations. Final message only.

tool_use

A prior tool call, replayed unchanged in assistant-turn history.

tool_result

Your reply to a tool_use: tool_use_id, content (string or text/image blocks), optional is_error.

thinking

An extended-thinking block, echoed back unchanged in assistant history.

redacted_thinking

An opaque redacted-thinking block, echoed back unchanged.

typeNotes
textPlain text content.
imagesource: base64 (image/jpeg, image/png, image/gif, image/webp) or a url. Final message only — see Images & documents above.
documentsource: base64 PDF, plain text, or a url; optional title, context, citations. Final message only.
tool_useA prior tool call, replayed unchanged in assistant-turn history.
tool_resultYour reply to a tool_use: tool_use_id, content (string or text/image blocks), optional is_error.
thinkingAn extended-thinking block, echoed back unchanged in assistant history.
redacted_thinkingAn opaque redacted-thinking block, echoed back unchanged.

Any block above may carry cache_control: { "type": "ephemeral", "ttl": "5m" | "1h" } to mark a prompt-caching breakpoint.

Errors & retries

Every error uses the Anthropic-style envelope - { "type": "error", "error": { "type", "message" } }. Stack traces are never leaked. Rate-limited and overloaded responses include a retry-after header; honor it with exponential backoff.

400invalid_request_error

Malformed body, failed schema validation, a media block outside the final message, or a per-request media limit exceeded

No
401authentication_error

Missing, invalid, expired, or revoked API key

No
403permission_error

Key is valid but not permitted to call the requested model

No
404not_found_error

Unknown model or unknown route

No
413request_too_large

Request body exceeds the configured size ceiling

No
429rate_limit_error

Per-key rate limit or concurrency ceiling exceeded (honor retry-after)

After backoff
500api_error

Unexpected internal failure (sanitized)

After backoff
504timeout_error

Runtime exceeded the configured timeout

After backoff
529overloaded_error

Runtime saturated or unavailable

After backoff
HTTPerror.typeWhenRetryable
400invalid_request_errorMalformed body, failed schema validation, a media block outside the final message, or a per-request media limit exceededNo
401authentication_errorMissing, invalid, expired, or revoked API keyNo
403permission_errorKey is valid but not permitted to call the requested modelNo
404not_found_errorUnknown model or unknown routeNo
413request_too_largeRequest body exceeds the configured size ceilingNo
429rate_limit_errorPer-key rate limit or concurrency ceiling exceeded (honor retry-after)After backoff
500api_errorUnexpected internal failure (sanitized)After backoff
504timeout_errorRuntime exceeded the configured timeoutAfter backoff
529overloaded_errorRuntime saturated or unavailableAfter backoff

Retry guidance:

  • •Retry 429, 529, and any 5xx with exponential backoff and jitter, honoring a retry-after header when the response includes one.
  • •Never retry 400, 401, or 404 — the request itself has to change first; retrying it unmodified fails identically.
  • •Never retry a streaming request after content has already reached the user — resend a fresh request instead of replaying a partial stream.

Differences from api.anthropic.com

Three things behave differently from the public Anthropic API:

  1. 1

    Only four API surfaces

    The gateway implements POST /v1/messages, GET /v1/models, GET /v1/account, and the public health checks. Other api.anthropic.com routes (batches, files, etc.) are not proxied and return not_found_error.

  2. 2

    usage token counts may be null

    input_tokens / output_tokens come straight from the runtime. When a runtime cannot report them the fields are null, the gateway never fabricates or estimates a count. Treat null as "unknown", not zero.

  3. 3

    Model names go through the alias layer

    The model you send is resolved through MODEL_ALIASES (e.g. "sonnet" → a real runtime id). An unmapped name returns not_found_error rather than being guessed. The live resolutions are listed below.

Other endpoints

POST /v1/messagesAPI key required

Create a message. Supports streaming via stream: true.

GET /v1/modelsAPI key required

Lists configured aliases + resolved runtime ids. Returns 401 without a key. Read this list rather than hardcoding model names — it changes as the operator updates aliases.

GET /v1/accountAPI key required

Caller identity (a non-reversible fingerprint, never a secret) and effective rate limits — useful for a settings screen.

GET /healthPublic

Lightweight liveness probe.

GET /health/deepPublic

Readiness probe (database + runtime). Can report degraded — runtime not initialized on a freshly started container: the runtime initializes lazily on the first /v1/messages call, so this is expected on a cold start, not a fault.

EndpointAuthNotes
POST /v1/messagesAPI key requiredCreate a message. Supports streaming via stream: true.
GET /v1/modelsAPI key requiredLists configured aliases + resolved runtime ids. Returns 401 without a key. Read this list rather than hardcoding model names — it changes as the operator updates aliases.
GET /v1/accountAPI key requiredCaller identity (a non-reversible fingerprint, never a secret) and effective rate limits — useful for a settings screen.
GET /healthPublicLightweight liveness probe.
GET /health/deepPublicReadiness probe (database + runtime). Can report degraded — runtime not initialized on a freshly started container: the runtime initializes lazily on the first /v1/messages call, so this is expected on a cold start, not a fault.

Response headers

Every response, success or error, carries these:

x-request-id

Unique per request. Quote it verbatim in bug reports — it is the fastest way to find the matching server-side log line.

anthropic-ratelimit-requests-limit

Your key's request-rate bucket capacity.

anthropic-ratelimit-requests-remaining

Whole tokens left in the current window.

anthropic-ratelimit-requests-reset

ISO-8601 timestamp the bucket next refills.

retry-after

Present on 429/529 responses only — seconds to wait before retrying.

HeaderNotes
x-request-idUnique per request. Quote it verbatim in bug reports — it is the fastest way to find the matching server-side log line.
anthropic-ratelimit-requests-limitYour key's request-rate bucket capacity.
anthropic-ratelimit-requests-remainingWhole tokens left in the current window.
anthropic-ratelimit-requests-resetISO-8601 timestamp the bucket next refills.
retry-afterPresent on 429/529 responses only — seconds to wait before retrying.

Available models (live)

Resolved live from GET /v1/models, never hard-coded. Send the left-hand name in the model field; it resolves to the runtime id on the right.

  • claude-haiku-5-5→claude-haiku-5-5
  • claude-sonnet-5-5→claude-sonnet-5-5
  • claude-opus-5-5→claude-opus-5-5
  • claude-fable-5-1→claude-fable-5-1
  • claude-opus-5→claude-opus-5
  • claude-sonnet-5→claude-sonnet-5
  • claude-fable-5→claude-fable-5
  • claude-opus-4-8→claude-opus-4-8
  • claude-opus-4-7→claude-opus-4-7
  • claude-sonnet-4-6→claude-sonnet-4-6
  • claude-opus-4-6→claude-opus-4-6
  • claude-opus-4-5-20251101→claude-opus-4-5-20251101
  • claude-haiku-4-5-20251001→claude-haiku-4-5-20251001
  • claude-sonnet-4-5-20250929→claude-sonnet-4-5-20250929
  • claude-haiku-4-5→claude-haiku-4-5
  • haiku→claude-haiku-4-5
  • opus→claude-opus-5
  • sonnet→claude-sonnet-5

The definitive machine-readable list is always GET /v1/models (authenticated).

Base URL & authFirst request (curl)SDKs (TS & Python)Tool useStreamingImages & documentsRequest referenceErrors & retriesDifferencesOther endpointsResponse headersAvailable models
Status pageAdmin console

© 2026 Private Claude-Compatible AI Gateway. Private deployment.