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.

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:

TierWhenRequests / minTokens / minQuestions / requestLargest top-up
Tier 0Sign up———$25
Tier 1$5 paid300300K32$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.

bash
curl https://api.saanora.com/v1/models \
  -H "Authorization: Bearer $SAANORA_API_KEY"
200 OK
{
  "object": "list",
  "data": [
    { "id": "clara-1", "object": "model", "owned_by": "saanora", "kind": "decision", "status": "live" }
  ]
}
Keep keys on your server. Anyone with a key can spend your credit. Never put it in browser or mobile code, and never commit it to a repository. If a key leaks, revoke it in the console: it stops working within 60 seconds.

4. Make a request

Send the record and your questions to POST /v1/decisions. The body has three required fields and one optional one:

FieldTypeDescription
modelstring, requiredThe model id: clara-1. An unknown id returns 404 model_not_found.
statestring or object, requiredThe 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.
questionsobject, requiredYour 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.
modestring, optionalfast (default) or consistent. Also accepted as a ?mode= query parameter. See the note below.

Each question has these fields:

FieldDescription
typenoul (yes/no), choice (one of several options) or score (a level on an ordered scale).
instructionsThe question itself. Write it so it can be answered from the record alone.
criterianoul: 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.
thresholdnoul 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 as an object
"state": {
  "ticket_id": 4812,
  "channel": "email",
  "plan": "business",
  "message": "Our nightly export has failed three nights in a row."
}
Fast or consistent. The default 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.
The first call after a quiet period. 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. 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:

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
}
FieldDescription
idUnique id of this decision, starting with dec_. Quote it when you contact support.
objectAlways decision.
createdWhen the decision was made, in Unix seconds.
modelThe model that answered.
modefast or consistent, as requested.
answersOne answer per question, keyed by your question ids. The shape depends on the question type, described below.
usageWhat the request read and cost, described below.
latency_msTime 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:

question
"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
}
answer
"failing": { "type": "noul", "noul": 0.996, "threshold": 0.9, "decision": true }

Choice answers

Score answers

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

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.

HeaderMeaning
x-request-idUnique id of the request, also sent on errors. Quote it to support.
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-tokensBilled input tokens per minute allowed for your organization.
x-ratelimit-remaining-tokensTokens left in the current 60-second window.
saanora-processing-msTotal 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:

401
{
  "error": {
    "type": "authentication_error",
    "code": "invalid_api_key",
    "message": "invalid or revoked API key",
    "param": null
  },
  "request_id": "req_5b0c2e8f9a1d4c7b3e6f8a20"
}
StatuscodeMeaning and fixRetry
400invalid_json, missing_state, missing_questions, missing_model, wrong_endpointThe 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
401missing_api_key, invalid_api_keyNo 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
402insufficient_creditYour credit cannot cover the hold for this request. Add credit in Billing. Nothing ran.After adding credit
404model_not_foundThe model id is unknown or not open yet. Use an id from GET /v1/models.No
413request_too_largeThe body is over 1 MB.No
422too_many_questions, invalid_question_type, invalid_criteria, invalid_modeA question or option is invalid. param names the field.No
400 or 422invalid_requestThe model rejected the request; the message is the model's own reason. Nothing is charged.No
429rate_limit_exceededYour organization is over its requests or tokens per minute.Yes, after Retry-After
429project_spend_limitThe project reached its monthly spend limit. Raise the limit or wait for the next UTC month.No
502model_unavailableThe model could not answer. Never charged.Yes, with backoff
503auth_unavailable, billing_unavailable, catalog_unavailableA platform service is briefly unavailable.Yes, with backoff

The rules in short:

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']}")
Failed requests are free. Before the model runs, a hold covering the worst-case cost is placed on your credit. If anything fails the hold is released, and a 502 model_unavailable is never charged. You pay only for answers you receive.

Next steps

Questions? Write to support@saanora.com with the x-request-id of the request you are asking about.