Authentication
Developer API keys: formats, the Bearer and x-api-key headers, scopes per route family, read-only keys, ceilings, rotation and hygiene.
Every API call is authenticated by a developer API key. A key belongs to one workspace, acts as the user who created it, and can be narrowed to the route families it needs, capped in questions per month and in requests per minute, made read-only, and given an expiry date. Narrowing is the point: the key you give to a client's application should be able to call the engine and nothing else.
Key format
A key looks like sk_live_ followed by 48 hexadecimal characters. Subsidia stores only a SHA-256 hash of it and a 16-character prefix (sk_live_xxxxxxxx) that the console shows to help you recognise it. The full key is returned once, at creation. There is no way to display it again; if it is lost, create a new key and revoke the old one.
Sending the key
| Header | Used by | Notes |
|---|---|---|
Authorization: Bearer sk_live_... | OpenAI SDKs, curl, most HTTP clients | The value must start with sk_live_ or sk_test_. Any other Bearer value is not treated as an API key. |
x-api-key: sk_live_... | Anthropic SDKs, the Subsidia SDK | Takes precedence over Authorization when both are present. |
curl https://api.subsidia.protypa.fr/v1/models -H "Authorization: Bearer $SUBSIDIA_API_KEY"curl https://api.subsidia.protypa.fr/v1/models -H "x-api-key: $SUBSIDIA_API_KEY"Creating and managing keys
The simplest way is the Gateway page of the console (https://subsidia.protypa.fr/console/gateway). Keys are also managed through /developer/api-keys, which is authenticated by a signed-in user session and not by an API key: a key can never create, list, change or revoke keys, including itself.
| Action | Request | Notes |
|---|---|---|
| Create | POST /developer/api-keys |
Body: name, scopes, monthlyQuestionLimit, ratePerMinute, expiresAt (ISO date). Returns 201 with the full key once. |
| List | GET /developer/api-keys |
Your active keys: id, name, prefix, scopes, ceilings, usageCount, lastUsedAt, createdAt, expiresAt. Never the secret. |
| Change | PATCH /developer/api-keys/:id |
Any of name, scopes, monthlyQuestionLimit, ratePerMinute. null removes a limit. The secret and the expiry cannot be changed. |
| Revoke | DELETE /developer/api-keys/:id |
204. The key stops working immediately and cannot be reactivated. |
| Usage | GET /developer/usage |
Request count and last use per key. |
Limits must be positive whole numbers up to 10 000 000. A bad scopes list is a 400 that names the accepted values.
# $SESSION_TOKEN is the signed-in user's session token, not an API keycurl https://api.subsidia.protypa.fr/developer/api-keys \ -H "Authorization: Bearer $SESSION_TOKEN" \ -H "x-workspace-id: $WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "name": "Client app - production", "scopes": ["engine", "knowledge"], "monthlyQuestionLimit": 2000, "ratePerMinute": 60, "expiresAt": "2027-06-30T00:00:00Z" }'Scopes
A scope grants a family of routes. Combine as many as the key needs. A restricted key that calls a route outside its families gets 403 scope_denied; a route that maps to no scope at all is refused for restricted keys too.
| Scope | Route families | Typical holder |
|---|---|---|
engine | /v1/chat/completions, /v1/messages, /v1/messages/count_tokens, /v1/models, /v1/ai/*, /v1/proofs/*, /v1/gateway/* | Any application that calls a model. See Gateway, Completions, Proof certificates. |
knowledge | /v1/knowledge-sources*, /v1/knowledge/* | Anything that uploads or queries documents. See Knowledge sources. |
proof | /v1/verify*, /v1/registre* | A checker or an audit tool. See Le Vérificateur and Le Registre. Exempt from the monthly question ceiling. |
agents | /v1/agents*, /v1/tools* | Applications that manage or chat with agents. See Agents. |
conversations | /v1/conversations* | Multi-agent debates. See Conversations. |
webhooks | /v1/webhooks* | Event subscriptions. See Webhooks. |
reflex | /engine/* | Automations. See Reflex. |
light | /light/* | The Light assistant. See Light. |
partner | /partner/* | Integrators managing client workspaces. See Integrators. |
readonly | not a route family | A restriction, described below. |
Read-only keys
Add readonly to the scopes and the key may only read. Any POST, PUT, PATCH or DELETE is refused with 403 read_only_key, with one deliberate exception: a few POST routes only ask a question and change nothing, so they stay open. These are /v1/chat/completions, /v1/messages, /v1/ai/complete, /v1/knowledge/query, /v1/verify, /light/ask and /light/web.
readonly is combined with capability scopes: ["engine", "readonly"] is a key that can ask the Gateway but cannot create anything. Inside the app a read-only key acts with the viewer role; every other key acts as the workspace owner.
Ceilings, limits and expiry
| Setting | Effect | Failure |
|---|---|---|
monthlyQuestionLimit | Caps the questions this key may consume per calendar month (UTC). The count can lag a burst by a few seconds, so treat it as a guard rail against a runaway integration, not as a meter. An email alert is sent as the key approaches its ceiling. | 402 API_KEY_BUDGET_EXCEEDED |
ratePerMinute | Caps requests per minute for this key, counted in the database so it holds across servers. Keys created without it have no per-key limit, but route-level limits still apply, see Rate limits. | 429 rate_limited with retry-after |
expiresAt | The key stops working at that instant. Set one for any key handed to a project with an end date. | 401 API key expired |
Rotating a key
- 1Create the replacement
Create a new key with the same scopes and limits. Both keys are valid at the same time, so nothing breaks.
- 2Deploy it
Roll the new value out to every place that uses the old one, through your secret manager.
- 3Check the old key is idle
In the console or with
GET /developer/usage, watch the old key'slastUsedAtstop moving. - 4Revoke the old key
Delete it. The next call made with it returns
401 Invalid or revoked API key.
Key hygiene
- One key per application and per environment, named after both, so a leak points at its source.
- Give each key the fewest scopes that work, a monthly ceiling, and an expiry when the project has an end.
- Keep keys in server-side code only. A key shipped to a browser or a mobile app is public; put your own backend in front of the Gateway instead.
- Never log the
Authorizationorx-api-keyheader. Log the key prefix if you need to tell keys apart. - Rotate on staff changes and after any suspected exposure; revoke first, investigate after.
- The proof certificates and the usage journal record which workspace acted, so a revoked key leaves a clear trail.
Authentication errors
| Status | Code or message | Meaning | What to do |
|---|---|---|---|
| 401 | API key required. Pass x-api-key header or Authorization: Bearer sk_live_xxx | No usable key was sent. | Check the header name and that the value starts with sk_live_. |
| 401 | Invalid or revoked API key | The key is unknown or was revoked. | Create a new key. A typo and a revoked key look the same on purpose. |
| 401 | API key expired | expiresAt has passed. | Create a replacement key. |
| 403 | scope_denied | The key lacks the scope for this route, or the route has no scope. | Add the scope named in the message, or use another key. |
| 403 | read_only_key | A read-only key tried to change something. | Use a key without readonly for writes. |
| 403 | client_suspended | The workspace was suspended by its integrator. | Contact your provider. Keys are kept and work again when access is restored. |
| 403 | provider_unpaid | The integrator that hosts this client workspace has an overdue invoice. | Contact your provider. Nothing is deleted. |
| 402 | API_KEY_BUDGET_EXCEEDED | The key reached its monthly question ceiling. | Raise the ceiling with PATCH /developer/api-keys/:id, or wait for the next month. |
| 429 | rate_limited | More requests per minute than the key allows. | Wait for retry-after seconds. |
The full catalogue, including the OpenAI and Anthropic error envelopes, is in Errors.