Menu
Quickstart
Make your first decision with Clara: create an account, add credit, create an API key, send one request and read every part of the answer.
You will send Clara one support message and ask it three typed questions about it: is an export failing (yes/no), which team should own it (a choice between two teams), and how severe the impact is (a score on a four-level scale). Clara returns a probability for every option you define, never free text.
To follow along you need one of: curl in a bash or zsh shell, Python 3 with requests (pip install requests), or Node.js 18 or later.
1. Create an account
Sign up with Google, or with an email address and password. Email sign-ups get a 6-digit verification code to confirm the address.
Every new user gets a personal organization with one project called Default, and you are its owner. The organization holds the credit and the rate limits; the project holds your API keys and usage. You can invite teammates and add projects later, see Organizations and projects.
2. Add credit
The platform is prepaid. Requests are paid from your organization's credit, and a request that cannot be covered returns 402 insufficient_credit without running, so add credit before your first call.
In the console open Billing, pick an amount and complete checkout. Top-ups start at $5: $5 buys about 10M input tokens, roughly 52,000 requests like the one below. A new organization can add up to $25 at a time; larger top-ups unlock as your tier rises.
- Price. Clara costs $0.50 per 1M input tokens. Output is $0, and there is no per-decision fee.
- Checkout. Polar is the merchant of record: it runs checkout, adds taxes where they apply, and issues the receipt and invoice. Both are under Billing, Payments.
- Credit never expires and is shared by every project in the organization.
Your first payment unlocks the API: it moves the organization from Tier 0, which is sign-up only, to Tier 1 the moment it completes. Limits are per organization, across all projects, keys and models:
| Tier | When | Requests / min | Tokens / min | Questions / request | Largest top-up |
|---|---|---|---|---|---|
| Tier 0 | Sign up | — | — | — | $25 |
| Tier 1 | $5 paid | 300 | 300K | 32 | $25 |
Higher tiers follow from total paid and time since your first payment. See Rate limits and Billing and credit.
3. Create an API key
In the console open API keys and click Create secret key. Give it a name that says where it runs (for example production-backend), choose the project, and create it.
Copy the key right away. It starts with sk_sa_ and is shown only once: the platform stores only a SHA-256 hash of it. If you lose it, revoke it and create a new one.
Store the key in an environment variable on the machine that will call the API. The samples on this page read SAANORA_API_KEY:
export SAANORA_API_KEY="sk_sa_..."Check that the key works by listing the live models. A 401 here means the variable is empty in this shell, the key was copied incompletely, or the key was revoked.
curl https://api.saanora.com/v1/models \
-H "Authorization: Bearer $SAANORA_API_KEY"{
"object": "list",
"data": [
{ "id": "clara-1", "object": "model", "owned_by": "saanora", "kind": "decision", "status": "live" }
]
}4. Make a request
Send the record and your questions to POST /v1/decisions. The body has three required fields and one optional one:
| Field | Type | Description |
|---|---|---|
model | string, required | The model id: clara-1. An unknown id returns 404 model_not_found. |
state | string or object, required | The record to decide about: a message, a ticket, a transcript, a form. JSON objects are rendered compactly. Up to 16K tokens; longer records are shortened in the middle. |
questions | object, required | Your question ids mapped to questions. 1 to 32 questions per request on Tier 1, 64 from Tier 2. The same ids come back as the keys of answers. |
mode | string, optional | fast (default) or consistent. Also accepted as a ?mode= query parameter. See the note below. |
Each question has these fields:
| Field | Description |
|---|---|
type | noul (yes/no), choice (one of several options) or score (a level on an ordered scale). |
instructions | The question itself. Write it so it can be answered from the record alone. |
criteria | noul: optional, {"true": "...", "false": "..."} describing what counts as yes and no (yes/no keys also work).choice: required, an object of option id to short description, 2 to 255 options. score: required, an array of levels ordered lowest first, at least 2. |
threshold | noul only, optional. A number from 0 to 1. When set, the answer also carries a boolean decision. |
Here is the request. The cURL tab sends all three questions. The Python and TypeScript tabs send two of them to stay short, and the Python tab's route_to stands in for your own code. A complete, runnable script follows in step 6.
curl --retry 5 https://api.saanora.com/v1/decisions \
-H "Authorization: Bearer $SAANORA_API_KEY" \
-H "Content-Type: application/json" \
-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"] }
}
}'The cURL sample is written for bash or zsh. On Windows, run it in Git Bash or WSL, or use the Python or TypeScript tab. state can also be a JSON object, which is convenient when your record already lives in your database:
"state": {
"ticket_id": 4812,
"channel": "email",
"plan": "business",
"message": "Our nightly export has failed three nights in a row."
}fast mode reads the record once for all questions: the lowest latency and cost. consistent reads the record again with each question, which gives steadier answers when options change or a question is reworded; it bills the tokens it reads, roughly the record times the number of questions, and counts them toward your tokens-per-minute limit. The response shape is the same in both.503 model_warming_up, never charged, with a Retry-After header saying how many seconds to wait. The samples retry after that wait for you. Keep a client timeout of 120 seconds; later requests answer in well under a second.5. Read the response
A successful request returns 200. This is the response to the cURL request above:
{
"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
}| Field | Description |
|---|---|
id | Unique id of this decision, starting with dec_. Quote it when you contact support. |
object | Always decision. |
created | When the decision was made, in Unix seconds. |
model | The model that answered. |
mode | fast or consistent, as requested. |
answers | One answer per question, keyed by your question ids. The shape depends on the question type, described below. |
usage | What the request read and cost, described below. |
latency_ms | Time the model spent, in milliseconds. |
Every answer is one of the options you defined, and every set of probabilities sums to 1. Clara never writes free text, so there is nothing to parse.
Yes/no answers
noul is the probability that the answer is yes. In the example, 0.996 means the export is almost certainly failing.
If you add a threshold to the question, the answer repeats it and adds decision, which is true when noul is at least the threshold. This question also uses criteria to define yes and no:
"failing": {
"type": "noul",
"instructions": "Is a scheduled export currently failing?",
"criteria": {
"true": "An export that runs on a schedule is failing now",
"false": "Exports work, or the message is about something else"
},
"threshold": 0.9
}"failing": { "type": "noul", "noul": 0.996, "threshold": 0.9, "decision": true }Choice answers
choice: the most likely option id, herebilling: the record mentions the finance team, so Clara leans to billing (0.66) over engineering (0.34).probabilities: a probability for every option id you defined.confidence: how far the top option stands above an even split, from 0 (all options equally likely) to 1 (certain). It is (top probability minus 1/n) divided by (1 minus 1/n), where n is the number of options. With two options and a top probability of 0.93: (0.93 minus 0.5) / (1 minus 0.5) = 0.86.
Score answers
probabilities: a probability for each level, keyed by its index as a string, lowest level first ("0"isnone).legend: maps each index back to the level text you sent.score: the expected level index, the sum of each index times its probability. Here 0 × 0.001 + 1 × 0.004 + 2 × 0.511 + 3 × 0.484 = 2.48, between high and critical, nearly halfway.confidence: the probability of the most likely level, here 0.51 forhigh.
Because score is an average it can fall between levels. To act on a single level, take the index with the highest probability and look it up in legend. Score scales are the least reliable of the three types, so prefer a yes/no or choice question when the decision allows it.
Usage and cost
input_tokens: the tokens you were billed for. The same number counts toward your tokens-per-minute limit.decisions: how many questions were answered.cost_usd: exactly what was deducted from your credit. Here 196 tokens at $0.50 per 1M tokens is $0.000098.
Every request that reaches the model also appears under Usage in the console with its time, project, key, model, mode, status, number of questions, tokens, latency, cost and request id, and you can export it as CSV. The record and the questions themselves are processed in memory and never stored, so they are never used for training.
Response headers
Successful responses carry these headers; x-request-id is also sent on errors. Run cURL with -i to see them.
| Header | Meaning |
|---|---|
x-request-id | Unique id of the request, also sent on errors. Quote it to support. |
x-ratelimit-limit-requests | Requests per minute allowed for your organization. |
x-ratelimit-remaining-requests | Requests left in the current 60-second window. |
x-ratelimit-reset-requests | Time until a request slot frees up, for example 12s. |
x-ratelimit-limit-tokens | Billed input tokens per minute allowed for your organization. |
x-ratelimit-remaining-tokens | Tokens left in the current 60-second window. |
saanora-processing-ms | Total time the request spent in the platform, including the model time in latency_ms. |
Act on the probabilities
Branch on the probabilities in code. A common pattern has three bands: act automatically above a high threshold, ask a person in the middle, and decline or fall back below. Pick the thresholds by checking Clara against a few hundred of your own labelled records before you rely on it. The script in the next step shows the pattern.
6. Handle errors and retries
Errors use standard HTTP status codes and a JSON body. code says what went wrong, param names the field at fault (or is null), and request_id matches the x-request-id header:
{
"error": {
"type": "authentication_error",
"code": "invalid_api_key",
"message": "invalid or revoked API key",
"param": null
},
"request_id": "req_5b0c2e8f9a1d4c7b3e6f8a20"
}| Status | code | Meaning and fix | Retry |
|---|---|---|---|
| 400 | invalid_json, missing_state, missing_questions, missing_model, wrong_endpoint | The body is not valid JSON, model, state or questions is missing, or the model is not a decision model (wrong_endpoint). Fix the request. | No |
| 401 | missing_api_key, invalid_api_key | No key was sent or it is not a complete sk_sa_ key (missing_api_key), or the key is unknown or revoked (invalid_api_key). Check that SAANORA_API_KEY is set in the shell that runs your code. | No |
| 402 | insufficient_credit | Your credit cannot cover the hold for this request. Add credit in Billing. Nothing ran. | After adding credit |
| 404 | model_not_found | The model id is unknown or not open yet. Use an id from GET /v1/models. | No |
| 413 | request_too_large | The body is over 1 MB. | No |
| 422 | too_many_questions, invalid_question_type, invalid_criteria, invalid_mode | A question or option is invalid. param names the field. | No |
| 400 or 422 | invalid_request | The model rejected the request; the message is the model's own reason. Nothing is charged. | No |
| 429 | rate_limit_exceeded | Your organization is over its requests or tokens per minute. | Yes, after Retry-After |
| 429 | project_spend_limit | The project reached its monthly spend limit. Raise the limit or wait for the next UTC month. | No |
| 502 | model_unavailable | The model could not answer. Never charged. | Yes, with backoff |
| 503 | auth_unavailable, billing_unavailable, catalog_unavailable | A platform service is briefly unavailable. | Yes, with backoff |
The rules in short:
- Retry
429 rate_limit_exceededafter the number of seconds in theRetry-Afterheader. - Retry
502and503with exponential backoff, for example 1, 2, 4 and 8 seconds, up to 5 tries. - Do not retry
429 project_spend_limitor other 4xx errors without changing something first: the same request fails the same way. - Failed requests are free, and they do not count toward your tokens-per-minute limit.
This complete script puts it together: it sends the three-question request with a 120-second timeout, retries what is safe to retry, raises a clear error otherwise, and branches on the answers.
# decide.py: pip install requests, then python decide.py
import os
import time
import requests
API_URL = "https://api.saanora.com"
API_KEY = os.environ["SAANORA_API_KEY"]
RETRYABLE = {429, 502, 503}
def decide(body: dict, tries: int = 5) -> dict:
"""POST /v1/decisions, retrying rate limits, model and platform hiccups."""
for attempt in range(tries):
last = attempt == tries - 1
try:
r = requests.post(
f"{API_URL}/v1/decisions",
headers={"Authorization": f"Bearer {API_KEY}"},
json=body,
timeout=120, # the first request after idle can be slow
)
except (requests.ConnectionError, requests.Timeout):
if last:
raise
time.sleep(2 ** attempt)
continue
if r.ok:
return r.json()
err = r.json()["error"]
retry = r.status_code in RETRYABLE and err["code"] != "project_spend_limit"
if not retry or last:
raise RuntimeError(
f"{r.status_code} {err['code']}: {err['message']} "
f"(param={err['param']}, request {r.headers.get('x-request-id')})"
)
time.sleep(float(r.headers.get("Retry-After") or 2 ** attempt))
body = {
"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", "threshold": 0.9,
"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"]},
},
}
d = decide(body)
a = d["answers"]
print("export failing:", a["failing"]["noul"], "decision:", a["failing"]["decision"])
team = a["team"]
p = team["probabilities"][team["choice"]]
if p >= 0.9:
print("route to", team["choice"])
elif p >= 0.6:
print("suggest", team["choice"], "and ask a person to confirm")
else:
print("leave it in the shared queue")
sev = a["severity"]
level = max(sev["probabilities"], key=sev["probabilities"].get)
print("severity:", sev["legend"][level], f"(expected index {sev['score']:.2f})")
u = d["usage"]
print(f"{u['input_tokens']} tokens, {u['decisions']} decisions, ${u['cost_usd']:.6f}, id {d['id']}")502 model_unavailable is never charged. You pay only for answers you receive.Next steps
- Guides: complete recipes for support triage, agent tool routing, moderation and invoice approval.
- Best practices: write questions Clara answers well, choose thresholds, and ask about missing information explicitly.
- Models: Clara's benchmarks and known limitations.
- API reference: every endpoint, field and header.
- Rate limits: how tiers rise and how to stay under your limits.
- Organizations and projects: invite your team, separate environments, cap spend per project.
- Billing and credit: top-ups, invoices and how each request is charged.
Questions? Write to support@saanora.com with the x-request-id of the request you are asking about.