The header
Authorization: Bearer sk_stav_live_...
Stav also accepts x-api-key with the same value, because the Anthropic SDK sends its credential that way. If both are present, Authorization wins.
# OpenAI SDK
OpenAI(api_key=KEY, base_url="https://api.stav.ai/v1")
# Anthropic SDK — sends x-api-key
Anthropic(api_key=KEY, base_url="https://api.stav.ai")
Key formats
| Prefix | What it is |
|---|---|
sk_stav_live_ | Team key, production |
sk_stav_test_ | Team key, sandbox |
sk_stav_user_live_ | User key, production |
sk_stav_user_test_ | User key, sandbox |
A key is shown once, at creation, and stored only as a hash. Legacy hm_* keys are no longer accepted — if you still have one in a config file, it will fail with invalid_api_key.
The environment is a property of the key, not of the URL. Both live and test keys go to the same host; the environment appears in /v1/auth/validate and on every log row.
Verify a key
curl https://api.stav.ai/v1/auth/validate \
-H "Authorization: Bearer $STAV_API_KEY"
This is the cheapest possible health check for a deployment — no tokens, no model, no cost. It returns the team, the key name, the key prefix, the environment and the scopes actually attached.
Scopes
A key can be restricted to a subset of endpoints:
| Scope | Grants |
|---|---|
inference:chat | POST /v1/chat/completions |
inference:messages | POST /v1/messages |
inference:embeddings | POST /v1/embeddings |
inference:models | GET /v1/models, |
A key with no scopes set has full access. Scopes are opt-in restriction, not opt-in permission. If you want least privilege, you have to say so explicitly.
A call outside the key's scopes returns 403:
{
"error": {
"message": "This API key does not have the 'inference:embeddings' scope.",
"type": "permission_error",
"code": "insufficient_scope"
}
}
The scope people get wrong
The OpenAI SDK calls GET /v1/models for model listings, and most IDE and agent integrations populate their model dropdown from it. A key scoped to inference:chat alone will complete fine and show an empty model list. If the tool needs a dropdown, add inference:models.
The Anthropic SDK does not call /v1/models, so inference:messages alone is genuinely sufficient there.
Rate limits
Limits are per key, enforced as requests per minute. The default is 60 rpm; your plan tier, your key's environment, and any per-key override set by an admin all feed into the effective number.
Every rejection carries the numbers:
HTTP 429
Retry-After: 3
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1785667200
{
"error": {
"message": "Rate limit exceeded. Retry after 2.4 seconds.",
"type": "rate_limit_error",
"code": "rate_limit_exceeded"
}
}
Back off on Retry-After. Note that rate_limit_exceeded has three sources — this per-key limiter, Stav's admission queue, and an upstream provider 429 passed through when routing had no failover candidate left. Only the first carries the X-RateLimit-* headers. See Errors.
Authentication failures
| Code | Status | Cause |
|---|---|---|
missing_api_key | 401 | No Authorization: Bearer and no x-api-key |
invalid_api_key | 401 | Wrong prefix, unknown key, or revoked key |
expired_api_key | 401 | Past expires_at, or a rotated key past its grace window |
The 401s carry a WWW-Authenticate: Bearer header. All five use the OpenAI-shaped error envelope.
Rotation
Rotating issues a new key and moves the old one to a deprecated state with a grace window. During the window both keys work, so you can roll a fleet without a flag day. After the window the old key returns expired_api_key.
Practical sequence:
- Rotate in the portal, copy the new key.
- Deploy the new key to your secret store.
- Watch Monitor → Requests filtered by key name until the old key shows no traffic.
- Let the grace window close, or revoke early.
Key hygiene
- One key per application, not per developer. Attribution and revocation are both per key, so a shared key destroys both.
- Use
sk_stav_test_in CI. Sandbox traffic is separated in analytics and can carry its own rate limit. - Scope keys you embed anywhere near a client.
inference:chat+inference:modelscovers almost every agent integration. - Send
X-Title. Not security, but the difference between "something is spending money" and "the nightly indexer is spending money".
If a key leaks
Revoke it in the portal — revocation takes effect on the next request, not on the next cache cycle. Then check Monitor → Requests filtered by that key: every call is logged with the model, the app, the token counts and the cost, so you can bound the exposure precisely rather than estimate it.
Next steps
- Discovering models
- Errors — the full error-code catalogue
- Response headers