Menu
Authentication
Every request to the Saanora API carries a secret API key. This page covers how to create a key, send it, keep it safe, rotate it, and fix the errors you get when something is wrong with it.
At a glance
| Topic | Detail |
|---|---|
| Header | Authorization: Bearer sk_sa_... (recommended) or X-API-Key: sk_sa_... |
| Format | sk_sa_ followed by 32 URL-safe characters |
| Where to create | Console > API keys > Create secret key |
| Scope | One project in one organization. Spends the organization's credit and shares its rate limits. |
| Who can create | Owners, admins and developers |
| Who can revoke | Owners and admins (any key), developers (keys they created) |
| Storage | Shown once, then stored only as a SHA-256 hash |
| Revocation | Takes effect within 60 seconds |
| Active keys | Up to 100 per organization |
| Errors | 401 missing_api_key, 401 invalid_api_key |
Create a key
Open API keys in the console and select Create secret key. Fill in two fields:
- Name: up to 64 characters. Name the service and environment that will use the key, for example
production-backendorstaging-worker. If you leave it empty the key is calleddefault. - Project: the project the key belongs to. It defaults to the project selected in the console. A key cannot be moved to another project later.
The console then shows the full key once. Copy it straight into your secrets manager or an environment variable on your server:
export SAANORA_API_KEY="sk_sa_..."The key works as soon as it is created. If you leave the page without copying it, the key cannot be shown again: revoke it and create another.
Key format
A key is the prefix sk_sa_ followed by 32 random characters drawn from A-Z, a-z, 0-9, - and _. The prefix makes keys easy to spot in code review and in secret scanners. Treat everything after the prefix as opaque: do not parse it or validate its length in your own code.
The console lists each key by its first 12 characters (for example sk_sa_Xy3k9Q…) together with its name, project, creator, creation time and when it was last used.
Send the key with each request
Put the key in the Authorization header with the Bearer scheme:
Authorization: Bearer sk_sa_...If your HTTP client or framework makes Authorization awkward to set, send the key in X-API-Key instead. Both headers work on every endpoint: POST /v1/decisions, POST /v1/systemone and GET /v1/models.
If a request carries both, an Authorization header with the Bearer scheme wins and X-API-Key is ignored. So when you use X-API-Key behind a proxy, make sure the proxy does not forward a Bearer token of its own.
To check that a key works, list the models it can call:
curl https://api.saanora.com/v1/models \
-H "Authorization: Bearer $SAANORA_API_KEY"A valid key returns 200 with the live models:
{
"object": "list",
"data": [
{ "id": "clara-1", "object": "model", "owned_by": "saanora", "kind": "decision", "status": "live" }
]
}GET /v1/models runs no model and is not billed, which makes it a good startup check for a new deployment or a freshly rotated key. A 200 proves the key is valid, but it does not check your credit or the project's spend limit, and it does not update the key's Last used time: only decision requests do.Keys belong to one project
Every key is created inside one project of one organization. That project decides how the key's requests are counted:
- Usage: requests show up under that project and that key in Usage, which breaks spend down by model, project and API key.
- Spend limit: if the project has a monthly spend limit (UTC month), requests made after it is reached fail with
429 project_spend_limit. - Credit: costs are paid from the organization's prepaid credit, which all projects share.
- Rate limits: your tier's requests per minute, tokens per minute and questions per request apply to the whole organization. Every key in every project draws on the same limits, so extra keys do not add capacity. See Rate limits.
A key can call the API and nothing else: it cannot sign in to the console, create keys, read billing or change settings. Use separate projects to separate environments, for example a production project with a spend limit and a staging project with a lower one. Archiving a project revokes all of its keys. See Organizations and projects.
Who can create and revoke keys
What you can do with keys depends on your role in the organization:
| Role | See keys | Create keys | Revoke keys |
|---|---|---|---|
| owner | Yes | Yes | Any key in the organization |
| admin | Yes | Yes | Any key in the organization |
| developer | Yes | Yes | Only keys they created |
| billing | Yes | No | No |
| reader | Yes | No | No |
Seeing a key means seeing its name, first 12 characters, project, creator and dates. Nobody, including owners and Saanora support, can see a full key after it is created. A member can always revoke a key they created, even if their role has since changed to billing or reader.
How keys are stored
Saanora stores only the SHA-256 hash of each key and its first 12 characters for display. On every request the gateway hashes the key you send and looks up the hash. This has two consequences:
- A leak of Saanora's database would not expose usable keys.
- A lost key cannot be recovered by anyone. Create a new key, deploy it, and revoke the lost one.
Keep keys on your server
Anyone who has a key can spend your organization's credit until the key is revoked. Call the API only from code you run on your own servers:
- Keep the key in an environment variable or a secrets manager. Never hard-code it or commit it to a repository, public or private.
- Never ship a key in browser JavaScript, a mobile app or a desktop app. In front-end frameworks, do not give the variable a public prefix such as
NEXT_PUBLIC_orVITE_: those values are bundled into the page. - Do not log request headers. Log the
x-request-idresponse header or therequest_idin an error body instead; support needs only that to find a request. - Use one key per service and environment, so you can revoke one without touching the others and see each one's spend in Usage.
- Set a monthly spend limit on each project to cap what a leaked key can spend.
If your browser or mobile app needs a decision, have it call your own backend. Your backend authenticates the user, calls Saanora with the key, and returns only what the client needs:
// app/api/triage/route.ts: runs on your server, so the key never reaches the browser.
export async function POST(req: Request) {
// Authenticate your own user here first: this route spends your Saanora credit.
const { message } = await req.json();
const r = await fetch("https://api.saanora.com/v1/decisions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SAANORA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "clara-1",
state: message,
questions: {
urgent: { type: "noul", instructions: "Does the customer say the problem blocks their work today?" },
},
}),
});
const body = await r.json();
if (!r.ok) {
console.error("saanora", r.status, body.error.code, body.request_id); // log the request id, never the key
return Response.json({ error: "decision failed" }, { status: 502 });
}
return Response.json({ urgent: body.answers.urgent.noul }); // probability of yes
}Rotate a key
Rotate a key on a regular schedule, when someone who had access to it leaves, and whenever it may have been exposed. An organization can have up to 100 active keys at once, so the old and new keys can both work while you switch over:
- 1. Create a new key in the same project. Include the date in its name, for example
production-backend-2026-10, so you can tell the two apart. - 2. Deploy it. Update the secret in your secrets manager or environment and redeploy or restart every service that reads it. During a rolling deploy, old and new instances keep working because both keys are valid.
- 3. Confirm the switch. On the API keys page, the new key's Last used time advances and the old key's stops. Usage shows the same under By API key.
- 4. Revoke the old key. It stops working within 60 seconds.
- 5. Watch your logs for
401 invalid_api_key. Any you see come from a service that still has the old key.
Revocation takes up to 60 seconds
To revoke a key, select Revoke next to it on the API keys page and confirm. The gateway caches key lookups for 60 seconds, so a revoked key can keep working for up to a minute, and requests it makes in that time are billed as normal. After that, every request with it returns 401 invalid_api_key.
Revocation is permanent: a revoked key cannot be restored. The console lists revoked keys separately with the time each was revoked. Archiving a project revokes all of its keys in the same way.
Authentication errors
Authentication fails with status 401 and error type authentication_error. Branch on error.code, not on the message text. Failed requests are never charged.
| Code | Means | Common causes |
|---|---|---|
missing_api_key | No key was found, or the value is not shaped like a key. | No Authorization or X-API-Key header. The environment variable is empty or unset, so the header reads Bearer or Bearer undefined. The Bearer scheme is missing (Authorization: sk_sa_...). The value is wrapped in quotes or has text before sk_sa_. |
invalid_api_key | The value looks like a key, but no active key matches it. | The key was revoked, directly or by archiving its project. A character was lost, added or changed when copying it. |
missing_api_key
Returned when the request carries no key, or the value does not start with sk_sa_ or is too short to be a key:
{
"error": {
"type": "authentication_error",
"code": "missing_api_key",
"message": "send your key as Authorization: Bearer sk_sa_...",
"param": null
},
"request_id": "req_5b0e9c2a7d4f81e3a6c09b12"
}Print the header your code actually sends (with the key masked) and compare it with Authorization: Bearer sk_sa_....
invalid_api_key
Returned when the key is well formed but unknown or revoked:
{
"error": {
"type": "authentication_error",
"code": "invalid_api_key",
"message": "invalid or revoked API key",
"param": null
},
"request_id": "req_e41a7c90b35d28f6017ac4d9"
}Compare the first 12 characters of the key you are sending with the active keys on the API keys page. If it is not there, or is listed under revoked keys, deploy an active key.
auth_unavailable
A 503 with type overloaded_error and code auth_unavailable means the key service could not be reached. It says nothing about your key. Retry with exponential backoff.
Handling these errors in code
Treat a 401 as a configuration problem: retrying the same key will not fix it, so stop and alert someone. Retry 503s with backoff:
import os
import time
import requests
class SaanoraAuthError(Exception):
"""The key is missing, malformed, unknown or revoked. Retrying will not help."""
def decide(body: dict, attempts: int = 5) -> dict:
for attempt in range(attempts):
r = requests.post(
"https://api.saanora.com/v1/decisions",
headers={"Authorization": f"Bearer {os.environ['SAANORA_API_KEY']}"},
json=body,
timeout=120, # the first request after idle can be slow while capacity starts
)
if r.ok:
return r.json()
if r.status_code == 401:
# missing_api_key or invalid_api_key: alert a person, do not retry
err = r.json()
raise SaanoraAuthError(f"{err['error']['code']} (request_id {err['request_id']})")
if r.status_code != 503:
r.raise_for_status() # 400, 402, 404, 413, 422, 429, 502: see the Errors page
# 503 (auth_unavailable and other temporary outages): back off and retry
if attempt < attempts - 1:
time.sleep(2 ** attempt)
raise RuntimeError(f"Saanora API still unavailable after {attempts} attempts")Every response, including errors, carries an x-request-id header; on an error it matches request_id in the body. Include it when you write to support@saanora.com. For every other status code, see Errors.