Stav is an EU-sovereign inference gateway. You point an SDK you already use at api.stav.ai, and Stav handles model selection, jurisdiction enforcement, failover, and per-request audit.
There are two wire contracts, both first-class:
- OpenAI —
POST /v1/chat/completions,/v1/embeddings,/v1/rerank,/v1/models - Anthropic —
POST /v1/messages
Use whichever your code already speaks. You do not have to pick a side, and you can mix them on the same API key.
(/v1/responses also exists, as a verbatim proxy for OpenAI models only — no routing.)
1. Get a key
Create a key in the Customer Portal under Connect → API keys. Keys look like this:
sk_stav_live_... production
sk_stav_test_... sandbox
Copy it once — Stav stores only a hash. If you lose it, rotate rather than recover.
export STAV_API_KEY="sk_stav_live_..."
Check that the key works before you write any code:
curl https://api.stav.ai/v1/auth/validate \
-H "Authorization: Bearer $STAV_API_KEY"
{
"valid": true,
"team_id": "…",
"team_name": "Acme AS",
"api_key_name": "prod-backend",
"api_key_prefix": "sk_stav_live_",
"environment": "PRODUCTION",
"scopes": ["inference:chat", "inference:models"]
}
2. Your first completion
Python (OpenAI SDK)
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["STAV_API_KEY"],
base_url="https://api.stav.ai/v1",
)
resp = client.chat.completions.create(
model="auto",
messages=[{"role": "user", "content": "Explain CLOUD Act exposure in two sentences."}],
)
print(resp.choices[0].message.content)
print("served by:",
TypeScript (OpenAI SDK)
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.STAV_API_KEY,
baseURL: "https://api.stav.ai/v1",
});
const resp = await client.chat.completions.create({
model: "auto",
messages: [{ role: "user", content: "Explain CLOUD Act exposure in two sentences." }],
});
Python (Anthropic SDK)
import os
from anthropic import Anthropic
client = Anthropic(
api_key=os.environ["STAV_API_KEY"],
base_url="https://api.stav.ai", # note: no /v1 — the SDK appends it
)
msg = client.messages.create(
model="claude-sonnet-4-5-20250929",
max_tokens=512,
messages=[{"role": "user", "content": "Explain CLOUD Act exposure in two sentences."}],
)
print(msg.content[0].text)
curl
curl https://api.stav.ai/v1/chat/completions \
-H "Authorization: Bearer $STAV_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Title: my-service" \
-d '{
"model": "auto",
"messages": [{"role": "user", "content": "Say hello in Norwegian."}]
}'
3. Read the receipt
Every inference response carries a set of X-Stav-* headers. They are the audit trail, and they are the fastest way to understand what the platform did with your request.
X-Stav-Request-Id: chatcmpl-8f3a…
X-Stav-Model: mistralai/mistral-nemo-instruct-2407
X-Stav-Provider: mistral
X-Stav-Model-Type: routed_commercial
X-Stav-Sovereignty-Level: L1
X-Stav-Route-Reason: Auto-routed: best score 78.4 based on team weights
X-Stav-Route-Time-Ms: 412.6
Read them from the SDK:
raw = client.chat.completions.with_raw_response.create(
model="auto",
messages=[{"role": "user", "content": "hei"}],
)
print(raw.headers["X-Stav-Model"])
print(raw.headers["X-Stav-Sovereignty-Level"])
resp = raw.parse()
The full list is in Response headers.
4. What model="auto" actually did
auto hands the request to the Smart Router, which scores every model your team is allowed to use on quality, cost and measured speed, applies your team's weights, and enforces your sovereignty floor. The chosen model comes back in the response model field and in X-Stav-Model; the one-line rationale is in X-Stav-Route-Reason.
You can also:
model="auto" # your team's default routing policy
model="@acme/eu-only" # a named router your team authored
model="claude-sonnet-4-5-20250929" # pin a specific model
See Smart Router.
5. Streaming
stream = client.chat.completions.create(
model="auto",
messages=[{"role": "user", "content": "Write a haiku about fjords."}],
stream=True,
stream_options={"include_usage": True},
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].
stream_options={"include_usage": true} is what makes the final usage chunk appear. Without it a streaming response reports no token counts at all.
6. Tell Stav who is calling
Send X-Title with the name of your application. It costs nothing and it makes every request attributable in Monitor → Requests, which is what you want the first time a bill or a latency spike needs explaining.
X-Title: my-service
If you have registered the app in Connect → Apps, send its UUID instead for an exact match:
X-Stav-App-Id: f192b999-5087-49c3-84a5-745a23a471c2
Three things that will surprise an OpenAI user
- Unknown request fields are rejected, not ignored. Stav refuses by name rather than dropping silently — if a parameter cannot be honoured for the model that served the request, you get a
400that says so. See Request parameters. - Send
max_tokens, notmax_completion_tokens. Stav renames it per provider for you. n > 1is not supported. Send one request per completion.
Next steps
- Authentication & API keys — scopes, environments, rotation, rate limits
- Discovering models — what is in the catalogue and how to query it
- Smart Router — how
autodecides - Sovereignty — the L0–L4 ladder and how to set a floor
- Reasoning — extended thinking across providers
- OpenAI SDK migration · Anthropic SDK migration
- API reference — every endpoint, with a Try-it panel