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

TopicDetail
HeaderAuthorization: Bearer sk_sa_... (recommended) or X-API-Key: sk_sa_...
Formatsk_sa_ followed by 32 URL-safe characters
Where to createConsole > API keys > Create secret key
ScopeOne project in one organization. Spends the organization's credit and shares its rate limits.
Who can createOwners, admins and developers
Who can revokeOwners and admins (any key), developers (keys they created)
StorageShown once, then stored only as a SHA-256 hash
RevocationTakes effect within 60 seconds
Active keysUp to 100 per organization
Errors401 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:

The console then shows the full key once. Copy it straight into your secrets manager or an environment variable on your server:

bash
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:

header
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:

200 response
{
  "object": "list",
  "data": [
    { "id": "clara-1", "object": "model", "owned_by": "saanora", "kind": "decision", "status": "live" }
  ]
}
Check a key without spending credit. 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:

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:

RoleSee keysCreate keysRevoke keys
ownerYesYesAny key in the organization
adminYesYesAny key in the organization
developerYesYesOnly keys they created
billingYesNoNo
readerYesNoNo

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:

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:

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:

Next.js route handler
// 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:

If a key has leaked. Revoke it first, then create and deploy a replacement. A short outage is cheaper than a leaked key spending your credit. Afterwards, check that key's spend in Usage.

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.

CodeMeansCommon causes
missing_api_keyNo 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_keyThe 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:

401 missing_api_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:

401 invalid_api_key
{
  "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:

python
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.