API reference

Everything the Saanora API accepts and returns: each endpoint, every request and response field with its type, default and limit, the three answer shapes, the response headers and every error code. To make your first call, start with the Quickstart and come back here for the details.

Base URL and endpoints

All requests go to https://api.saanora.com over HTTPS. Bodies are JSON, sent with Content-Type: application/json and limited to 1 MB.

MethodPathWhat it does
POST/v1/decisionsAnswers typed questions about one record, with a probability for every option. model is required.
POST/v1/systemoneThe same request and response for clients written for the open System One format, such as JevBench. model is optional and defaults to clara-1.
GET/v1/modelsLists the live models you can pass as model.

Every response, including errors, carries an x-request-id header. Error bodies repeat it as request_id. Quote it when you write to support@saanora.com.

Cold starts and timeouts. Clara scales down when idle, so after a quiet period it needs about a minute to start: the first request waits about 60–80 seconds and is then answered. Consistent mode follows about 50 seconds later; until then a consistent request gets 503 model_warming_up, never charged, with a Retry-After header saying how many seconds to wait. Retry after that wait, and use a client timeout of 120 s, as in the examples below.

Authentication

Every request needs a secret API key. Create one in the console under API keys and send it as a bearer token:

headers
Authorization: Bearer sk_sa_...
Content-Type: application/json

Clients that cannot set Authorization can send the key as X-API-Key: sk_sa_... instead.

Keep keys on your server. Anyone with a key can spend your credit. Never ship it in browser or mobile code. More in Authentication.

Create a decision

POST/v1/decisions

Send one record (state) and a set of typed questions about it. You get back, for every question, a probability over the options you defined. Answers are always one of your options, probabilities always sum to 1, and the model never writes free text.

Request body

FieldTypeRequiredDescription
modelstringYesA decision model id from GET /v1/models, today clara-1. Missing: 400 missing_model. Unknown or not yet open (such as mark-1): 404 model_not_found.
statestring or objectYesThe record to decide about: a ticket, a conversation, a database row. An object is rendered compactly as text, so you can send a row as it is. Up to 16K tokens; a longer record is shortened in the middle, so the start and end are kept. English only. Missing: 400 missing_state.
questionsobjectYesYour question id → a question object. 1 to 32 questions on Tier 1, 64 from Tier 2. The ids come back as the keys of answers. Missing or empty: 400 missing_questions. Too many: 422 too_many_questions.
mode"fast" | "consistent"NoDefault fast. See Modes. Also accepted as a query parameter, ?mode=consistent; if both are set, the body wins. Any other value: 422 invalid_mode.

A body over 1 MB returns 413 request_too_large; a body that is not JSON returns 400 invalid_json.

Question object

Each value in questions describes one question. Write instructions so it can be answered from the record alone.

FieldTypeApplies toDescription
type"noul" | "choice" | "score"AllYes/no, one of several options, or a level on an ordered scale. Any other value: 422 invalid_question_type.
instructionsstringAllThe question itself, in plain English.
criteriaobject or arrayAllnoul: optional object describing yes and no. choice: required object, option id → short description, 2 to 255 options. score: required array of levels, lowest first, 2 to 255 levels. Wrong shape or count for choice or score: 422 invalid_criteria.
thresholdnumber from 0 to 1noulOptional. When set, the answer also carries a boolean decision. A value outside 0 to 1 is ignored.

Yes/no questions (noul)

Use noul for anything with a yes or no answer. criteria is optional: add it when yes and no need defining, as an object with "true" and "false" keys ("yes" and "no" are also accepted). Add threshold to have the API apply your cut-off for you.

noul question
"refund": {
  "type": "noul",
  "instructions": "Is the customer asking for their money back?",
  "criteria": {
    "true": "The customer asks for a refund or a chargeback",
    "false": "Any other request, including questions about the refund policy"
  },
  "threshold": 0.9
}

Choice questions

Use choice when exactly one of several options applies. criteria maps each option id to a short description; the ids are what you get back. 2 to 255 options.

choice question
"intent": {
  "type": "choice",
  "instructions": "What does the customer want to do with their subscription?",
  "criteria": {
    "cancel": "End the subscription",
    "upgrade": "Move to a bigger plan",
    "support": "Get help with a problem, without changing the plan"
  }
}

Score questions

Use score for an ordered scale. criteria is an array of level descriptions, lowest first, with 2 to 255 levels. Levels are referred to by their index: 0 for the first, 1 for the next, and so on. Score scales are the least reliable question type, so when a decision can be phrased as yes/no or as a choice, prefer that.

score question
"severity": {
  "type": "score",
  "instructions": "How severe is the business impact?",
  "criteria": ["none", "low", "high", "critical"]
}

Modes: fast and consistent

Both modes return the same response shape. The difference is how the record is read and what is billed.

fast (default)consistent
ReadingThe record once; every question reuses that readingThe record again with each question, so each reading takes that question into account
Other questions in the requestNever change an answerNever change an answer
Options changed or a question rewordedAnswers can move moreAnswers hold steadier
Billed tokensThe record plus each question, onceThe tokens it reads: roughly the record times the number of questions
Tokens per minuteCounts the billed tokensCounts the billed tokens, so it uses your TPM faster
Latency and costLowestHigher; cost grows with the number of questions
JevBench-mini81.883.6

For example, a 2,000-token record with 8 questions bills a little over 2,000 tokens in fast mode (about $0.001 at $0.50 per 1M) and roughly 16,000 tokens in consistent mode (about $0.008). In both modes each question is answered on its own, so adding or removing a question never changes the others. Use consistent when steadier probabilities are worth the extra tokens: thresholds, audits, and choice questions whose options change over time, such as tool menus.

Example request

Three questions, one of each type, about a support message. The Python and TypeScript versions check for errors and print each answer.

curl https://api.saanora.com/v1/decisions \
  -H "Authorization: Bearer $SAANORA_API_KEY" \
  -H "Content-Type: application/json" \
  --max-time 120 \
  -d '{
  "model": "clara-1",
  "state": "Customer writes: our nightly export has failed three nights in a row and the finance team needs it by 9am.",
  "questions": {
    "failing":  { "type": "noul",   "instructions": "Is a scheduled export currently failing?" },
    "team":     { "type": "choice", "instructions": "Which team should own this?",
                  "criteria": { "engineering": "Fixes broken product functionality",
                                "billing": "Handles invoices, charges and refunds" } },
    "severity": { "type": "score",  "instructions": "How severe is the business impact?",
                  "criteria": ["none", "low", "high", "critical"] }
  }
}'

Response

A successful request returns 200 OK with a decision object. This is the response to the example above:

200 OK
{
  "id": "dec_1eab8757a66cc4fba1e7d9f7",
  "object": "decision",
  "created": 1792431238,
  "model": "clara-1",
  "mode": "fast",
  "answers": {
    "failing": { "type": "noul", "noul": 0.996 },
    "team": {
      "type": "choice", "choice": "billing", "confidence": 0.317,
      "probabilities": { "engineering": 0.341, "billing": 0.659 }
    },
    "severity": {
      "type": "score", "score": 2.48, "confidence": 0.511,
      "probabilities": { "0": 0.001, "1": 0.004, "2": 0.511, "3": 0.484 },
      "legend": { "0": "none", "1": "low", "2": "high", "3": "critical" }
    }
  },
  "usage": { "input_tokens": 191, "decisions": 3, "cost_usd": 0.000096 },
  "latency_ms": 279.6
}
FieldTypeDescription
idstringUnique id of this decision, starting dec_.
objectstringAlways decision.
createdintegerWhen the decision was created, in Unix seconds.
modelstringThe model that answered, for example clara-1.
modestringThe mode that ran: fast or consistent.
answersobjectOne entry per question, keyed by your question ids. The shape depends on the question type: see the answer tables below.
usage.input_tokensintegerBilled input tokens. In consistent mode, everything read across all questions. The same number counts toward your tokens per minute.
usage.decisionsintegerNumber of questions answered.
usage.cost_usdnumberExactly what was deducted from your credit, in US dollars.
latency_msnumberModel time for this request, in milliseconds. Total time in the platform is in the saanora-processing-ms header.

Clara costs $0.50 per 1M input tokens and $0 for output, with no per-decision fee. The example above billed 191 tokens: 191 × $0.50 / 1,000,000 = $0.0000955, rounded up to $0.000096. Each request's cost is rounded up to the nearest $0.000001. Before the model runs, a hold covering the worst case is placed on your credit; the exact cost is deducted afterward. If the hold cannot be covered, the request fails with 402 insufficient_credit and nothing runs. Failed requests are free. See Billing and credit.

Yes/no answer

FieldTypeDescription
typestringnoul.
noulnumber, 0 to 1Probability that the answer is yes. The probability of no is 1 - noul.
thresholdnumberOnly when the question set threshold: your value, echoed back.
decisionbooleanOnly when the question set threshold: noul >= threshold.

The refund question above, with threshold 0.9, could come back as:

noul answer
"refund": { "type": "noul", "noul": 0.93, "threshold": 0.9, "decision": true }
Yes/no answers are committed. Clara always takes a side on a yes/no question: noul is at most 0.199 or at least 0.801, never in between. A threshold anywhere from 0.2 to 0.8 therefore behaves exactly like 0.5. To separate confident answers from borderline ones, put your threshold above 0.8, for example 0.9 or 0.95, and send everything between 0.801 and it to review.

Choice answer

FieldTypeDescription
typestringchoice.
choicestringThe most likely option id. Always one of your criteria keys.
probabilitiesobjectOption id → probability, for every option. Sums to 1.
confidencenumber, 0 to 1(max p - 1/n) / (1 - 1/n), where n is the number of options. 1 when one option has all the probability, 0 when all options are equally likely, so it compares fairly across questions with different numbers of options.

For the intent question above, with 3 options and 0.94 on support: (0.94 - 1/3) / (1 - 1/3) = 0.91.

choice answer
"intent": {
  "type": "choice",
  "choice": "support",
  "probabilities": { "cancel": 0.04, "upgrade": 0.02, "support": 0.94 },
  "confidence": 0.91
}

Score answer

FieldTypeDescription
typestringscore.
scorenumberThe expected level index: the sum of index × probability. Between 0 and the number of levels minus 1, and usually not a whole number.
legendobjectLevel index (as a string) → the level description you sent.
probabilitiesobjectLevel index → probability, for every level. Sums to 1.
confidencenumber, 0 to 1The probability of the most likely level.

For the severity question in the example: 0 × 0.001 + 1 × 0.004 + 2 × 0.511 + 3 × 0.484 = 2.48, so the impact sits between "high" and "critical", nearly halfway. The most likely single level is the key with the highest probability, here "2" ("high").

score answer
"severity": {
  "type": "score",
  "score": 2.48,
  "legend": { "0": "none", "1": "low", "2": "high", "3": "critical" },
  "probabilities": { "0": 0.001, "1": 0.004, "2": 0.511, "3": 0.484 },
  "confidence": 0.511
}

Response headers

HeaderMeaning
x-request-idUnique id of this request. Also sent on errors.
x-ratelimit-limit-requestsRequests per minute allowed for your organization.
x-ratelimit-remaining-requestsRequests left in the current 60-second window.
x-ratelimit-reset-requestsTime until a request slot frees up, for example 12s.
x-ratelimit-limit-tokensInput tokens per minute allowed for your organization.
x-ratelimit-remaining-tokensInput tokens left in the current 60-second window.
saanora-processing-msTotal time the request spent in the platform, in milliseconds. Subtract latency_ms to see the time outside the model.
Retry-AfterOn 429 rate_limit_exceeded only: seconds to wait before retrying.

The x-ratelimit-* and saanora-processing-ms headers come with every response that reached the model, successful or not. A 429 rate_limit_exceeded carries the x-ratelimit-* headers and Retry-After. Any other error carries only x-request-id.

List models

GET/v1/models

Returns the live models. Use it to check that a key works, or to confirm a model id before you send it. It needs the same API key as every other endpoint.

curl https://api.saanora.com/v1/models \
  -H "Authorization: Bearer $SAANORA_API_KEY"

The response:

200 OK
{
  "object": "list",
  "data": [
    { "id": "clara-1", "object": "model", "owned_by": "saanora", "kind": "decision", "status": "live" }
  ]
}
FieldTypeDescription
objectstringAlways list.
data[].idstringThe id to pass as model.
data[].objectstringAlways model.
data[].owned_bystringAlways saanora.
data[].kindstringWhat kind of model it is. Clara is a decision model, served by /v1/decisions and /v1/systemone.
data[].statusstringThe model's availability, for example live.

Mark (mark-1), the language model behind Saanora Chat, is coming soon. Until it opens, requests to it return 404 model_not_found. See Models for benchmarks and known limitations.

System One compatibility

POST/v1/systemone

For clients written for the open System One format, such as JevBench. Point the client at https://api.saanora.com/v1/systemone and send your key in the Authorization header.

cURL
curl https://api.saanora.com/v1/systemone \
  -H "Authorization: Bearer $SAANORA_API_KEY" \
  -H "Content-Type: application/json" \
  --max-time 120 \
  -d '{
  "state": "Customer writes: our nightly export has failed three nights in a row.",
  "questions": {
    "failing": { "type": "noul", "instructions": "Is a scheduled export currently failing?" }
  }
}'

Errors

Errors use standard HTTP status codes and a JSON body. Branch on error.code; message is written for people and can change.

422 Unprocessable Entity
{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_criteria",
    "message": "choice needs criteria with 2 to 255 options",
    "param": "questions.team.criteria"
  },
  "request_id": "req_5b0c2e8f9a1d4c7b3e6f8a20"
}
FieldTypeDescription
error.typestringThe class of error, fixed by the status code (table below).
error.codestringThe specific reason, such as invalid_criteria.
error.messagestringA readable explanation of what to fix.
error.paramstring or nullThe field that caused the error, such as questions.team.criteria, or null when no single field did.
request_idstringSame value as the x-request-id header.
Statuserror.type
400, 413, 422invalid_request_error
401authentication_error
402insufficient_credit
404not_found_error
429rate_limit_error
502api_error
503overloaded_error

Error codes

StatuscodeMeaning
400invalid_jsonThe body is not valid JSON.
400missing_stateThe body has no state field, or is not a JSON object.
400missing_questionsquestions is missing, is not an object, or is empty.
400missing_modelNo model on /v1/decisions. (/v1/systemone defaults to clara-1.)
400wrong_endpointThe model is not a decision model, so this endpoint does not serve it.
401missing_api_keyNo key was sent, or it is not an sk_sa_ key.
401invalid_api_keyThe key is unknown or has been revoked.
402insufficient_creditYour credit cannot cover the hold for this request. Nothing ran.
404model_not_foundThe model id does not exist or is not available yet (mark-1 until it opens).
404not_foundThe path does not exist, for example /v1/decision. A known path with the wrong method, such as GET /v1/decisions, returns 405 with the same error body.
413request_too_largeThe body is over 1 MB.
422too_many_questionsMore questions than your tier allows in one request (32 on Tier 1, 64 from Tier 2).
422invalid_question_typeA question is not an object, or its type is not noul, choice or score. param is questions.<id>.type.
422invalid_criteriaChoice criteria are not an object of 2 to 255 options, or score criteria are not a list of 2 to 255 levels.
422invalid_modemode is not fast or consistent.
400 or 422invalid_requestThe model rejected a request that passed the checks above, for example noul criteria that are not an object. The message is the model's own reason. Fix the request before retrying.
429rate_limit_exceededYour organization is over its requests or tokens per minute. Wait for Retry-After.
429project_spend_limitThe project reached its monthly spend limit (UTC month). Retrying does not help: raise the limit or wait for the next month.
502model_unavailableThe model could not answer. Never charged. Retry with backoff.
503auth_unavailableThe key service is briefly unavailable. Retry with backoff.
503billing_unavailableThe billing service is briefly unavailable. Retry with backoff.
503catalog_unavailableThe model catalog is briefly unavailable. Retry with backoff.
Failed requests are free. You are never charged for a request that fails, including 502 model_unavailable. Retry 429 rate_limit_exceeded, 502 and 503; fix the request, credit or spend limit before retrying anything else. Retry strategy is in Errors.

Limits

LimitValueWhen exceeded
Request body1 MB413 request_too_large
Record (state)About 16K tokens, less when a question has a long option listNo error: the middle is shortened and billed as read
Questions per request32 on Tier 1, 64 from Tier 2422 too_many_questions
Choice options2 to 255422 invalid_criteria
Score levels2 to 255422 invalid_criteria
Requests per minuteBy tier, table below429 rate_limit_exceeded
Tokens per minuteBy tier, table below429 rate_limit_exceeded
Project monthly spendOptional, set per project (UTC month)429 project_spend_limit
CreditYour prepaid balance402 insufficient_credit

Rate limits apply to the whole organization, across all projects, keys and models. Both are sliding 60-second windows. Tokens per minute counts billed tokens, the same number as usage.input_tokens; failed or refused requests do not count. Your tier rises automatically the moment both conditions are met.

TierQualifies atRequests/minTokens/minQuestions/request
Tier 0Sign up (no API access; requests return 402)———
Tier 1$5 paid300300K32
Tier 2$50 paid, 7+ days since first payment700700K64
Tier 3$250 paid, 14+ days since first payment1,5001.5M64
Tier 4$1,000 paid, 30+ days since first payment2,5002.5M64
Tier 5$2,500 paid, 60+ days since first payment5,0005M64

Beyond Tier 5, write to support@saanora.com. More in Rate limits.