Authentication
Bearer tokens, org scoping, security.
Every API call needs a bearer token in the Authorization header.
Authorization: Bearer sk-sovr-…Creating an API key
- In the UI: Settings → API keys.
- Click "New API key" and give it a name.
- The token is shown in clear text once.
- Store it safely straight away (vault, environment variable, secret manager).
Important: keys are only ever stored server-side as a hash. If you lose a key you have to create a new one — the old one cannot be recovered.
Permissions (scopes)
When you create a key you choose which capabilities it may use — we scope by capability, not by individual model. All of them are enabled by default; narrow them down per use case (for example a TTS-only key with no chat access).
| Scope | Allows | Endpoint |
|---|---|---|
chat | Chat completions | POST /v1/chat/completions |
embeddings | Embeddings | POST /v1/embeddings |
rerank | Re-ranking | POST /v1/rerank |
speech | Text-to-speech | POST /v1/audio/speech |
transcribe | Speech-to-text | POST /v1/audio/transcriptions |
mcp | Platform MCP tools | POST /mcp/mcp |
GET /v1/models and GET /v1/me need no particular scope — any valid key
may read the catalogue and query its own permissions. If a key is missing the
scope an endpoint needs, that endpoint answers 403 (permission_error,
message key lacks <scope> scope). Existing keys keep full access;
restricting is a deliberate choice at creation time.
Query the permissions instead of guessing
curl https://sovrgpt.com/api/v1/me -H "Authorization: Bearer $SOVR_KEY"{
"object": "api_key",
"id": "…",
"org_id": "…",
"name": "Dictation integration",
"key_prefix": "sovr_a1b2c3d4",
"expires_at": null,
"scopes": ["transcribe"],
"permissions": [
{ "scope": "chat", "label": "Chat", "endpoint": "POST /v1/chat/completions", "granted": false },
{ "scope": "embeddings", "label": "Embeddings", "endpoint": "POST /v1/embeddings", "granted": false },
{ "scope": "rerank", "label": "Re-Ranking", "endpoint": "POST /v1/rerank", "granted": false },
{ "scope": "speech", "label": "Text-to-Speech", "endpoint": "POST /v1/audio/speech", "granted": false },
{ "scope": "transcribe", "label": "Speech-to-Text", "endpoint": "POST /v1/audio/transcriptions","granted": true },
{ "scope": "mcp", "label": "MCP-Tools", "endpoint": "POST /mcp/mcp", "granted": false }
]
}permissions lists every possible scope, not just the granted ones —
otherwise you would have to know the full set of values in order to notice that
one is missing. During setup it therefore says outright: "this key may
transcribe, but it may not format."
The response never contains the key hash and never the identity of the
person who created the key. key_prefix is the display value meant for this:
it lets you recognise the key without disclosing it.
Org scoping
Every key belongs to an org. That means:
- Usage is billed to the org, not to the individual user.
- Logs appear under
Org → Usage. - Removing someone from the org does not automatically disable their org-scoped keys — those keep working until an admin revokes them. → When offboarding, always revoke every key belonging to the departing user.
Revoking a key
Settings → API keys → "Revoke". Takes effect immediately. A revoked key
returns this for every subsequent call:
HTTP/1.1 401 Unauthorized
{ "error": { "type": "invalid_request_error", "message": "invalid api key" } }Error cases
| Status | error.type | Meaning |
|---|---|---|
| 401 | invalid_request_error | Header missing, key invalid, or key revoked. |
| 403 | permission_error | The key exists but lacks the scope this endpoint requires (key lacks <scope> scope). |
| 429 | rate_limit_exceeded | Rate limit reached. Respect the Retry-After header. |
| 503 | service_unavailable | Model endpoint is cold and starting up. Retry with backoff. |
Best practices
- Never commit keys.
.gitignoremust cover.env*and*.local. - Never ship keys to the client (browser, mobile app). Always go through a backend proxy.
- Never put keys in URL query strings — they end up in server logs.
- One key per use case. That way you can revoke precisely.
- Rotate every 90 days.
CORS
The API is not meant for direct browser calls. If you try anyway, you get no
Access-Control-Allow-* headers from us and the call fails. That is deliberate
and protects you from accidentally leaking a key.
For browser apps: run your own backend proxy that keeps the keys server-side.
OAuth (planned)
For third-party apps that want to act on behalf of a SovrGPT user, an OAuth 2.0 authorization-code flow is planned for Q4/2026. Until then, API keys only.