Data handling

Where your data goes when you call the API or use the console: what is only processed, what is stored, who can read it, how long it is kept and how to have it deleted. Use it to answer a security review, a customer questionnaire or your own privacy notice.

At a glance

The content of a request, meaning the record and the questions, is never stored. What is stored is the metadata needed to show your usage and charge for it, plus the details needed to run your account.

DataStoredDetails
Records (state) and questionsNoProcessed in memory while the request runs, then gone.
AnswersNoReturned to you in the response. No copy is kept.
Request metadataYesTime, project, key, model, mode, status, counts, tokens, latency, cost and request id. No content.
Account detailsYesName, email, and avatar if you sign in with Google.
API keysHash and prefixA SHA-256 hash and the first 12 characters. Never the full key.
Card detailsNoHandled by Polar, the merchant of record. They never reach us.

Your requests

What is processed, not stored

A request to POST /v1/decisions or POST /v1/systemone reaches the API over HTTPS. The API checks your key, your credit and your limits, then passes the body to the model. The model reads the record, answers the questions, and the answers are sent back to you in the response.

The state and questions are held in memory only for that work. They are never written to the database, and neither are the answers. That has three consequences:

Keep your own copy. If you need to know later what was asked and what Clara answered, for an audit trail, a dispute or a model evaluation, store it on your side when the response arrives. Keeping your own records has a complete example.

What is stored for each request

Each request that reaches the model writes one row of metadata. This is what the Usage page, the CSV export and your charges are built from. Nothing in it comes from the record, the questions or the answers.

FieldWhat it holdsCSV column
TimeWhen the request completed, in UTC.time_utc
OrganizationThe organization the key belongs to.One organization per export
ProjectThe project the key belongs to.project (its name)
API keyA reference to the key that sent it, never the key itself.api_key (its name)
Endpoint/v1/decisions or /v1/systemone.endpoint
ModelThe model id, for example clara-1.model
Modefast or consistent.mode
Status200 when answered, otherwise an error status.status
QuestionsHow many questions were answered (0 when the model could not answer).questions
Input tokensBilled tokens, the same number as usage.input_tokens in the response.input_tokens
Output tokensAlways 0 for Clara, which never writes free text.Not exported
LatencyModel time in milliseconds, the same as latency_ms in the response. The CSV rounds it to whole milliseconds.latency_ms
CostExactly what was deducted, the same as usage.cost_usd.cost_usd
Request idThe same value as the x-request-id response header.request_id
Billing referenceAn internal link to the credit hold the request was paid from.Not exported

A request the model could not answer is still recorded, with its status, 0 tokens and a cost of $0, so you can see failures in Usage. A request refused before the model runs, for example for a missing or invalid key, an invalid body, a rate limit, a spend limit or insufficient credit, does not write a usage row. Its error response still carries a request_id for your logs.

Example: what you send and what we keep

This request carries a customer's name, email and message. The first tab is what you send, the second is what you get back, and the third is the row the platform keeps about it, as it appears in the CSV export from Console > Usage. The export leaves out the organization, the output token count and the billing reference.

{
  "model": "clara-1",
  "state": {
    "customer": "Dana Whitfield",
    "email": "dana@example.com",
    "message": "My order 48213 arrived broken. I would like my money back.",
    "order_date": "2026-10-02",
    "order_status": "delivered"
  },
  "questions": {
    "wants_refund": {
      "type": "noul",
      "instructions": "Is the customer asking for a refund?"
    },
    "topic": {
      "type": "choice",
      "instructions": "What is the message mainly about?",
      "criteria": {
        "damaged": "The item arrived damaged or broken",
        "late": "The order is late or has not arrived",
        "other": "Anything else"
      }
    }
  }
}

The name, email, message and order details are not in the stored row. Neither are the question text, the options or the probabilities. Only the counts, the timing, the cost and the ids remain.

Your account

To run your account and your organizations, the platform stores:

DataWhat is stored
ProfileYour name, your email address, your avatar if you sign in with Google, and when you signed up. You can change your display name in Settings.
PasswordHandled and stored hashed by our authentication provider. We never see it. Email sign-ups are confirmed with a 6-digit verification code.
OrganizationsName, who created it, its members and each member's role.
ProjectsName, the optional monthly spend limit, and when it was archived.
InvitationsThe invitee's email, the role offered, who sent it, and when it was sent, accepted or revoked.

The console uses only strictly necessary cookies: your sign-in session, and the organization and project you last selected. There are no advertising or analytics cookies.

API keys

A key is generated when you create it and shown to you once. The platform then keeps only:

On each request the API hashes the key you send and looks up the hash. Lookups are cached for 60 seconds, which is why a revoked key stops working within 60 seconds. Revoked keys stay listed in the console, so past usage still shows which key sent each request.

We cannot recover a key. Because only the hash is stored, nobody at Saanora can show you a lost key or see one that leaked. Revoke it under API keys and create a new one.

Payments

Polar is the merchant of record. It runs checkout, collects taxes, and issues receipts and invoices. Your card details go to Polar and never reach our database.

For each payment, the platform stores the order record Polar sends when the payment completes or is refunded, including the order id, its status, the net, total and refunded amounts, the currency and who paid. It also keeps the organization's credit balance, lifetime paid amount, tier and tier history, and a credit history: every top-up and refund, and every credit hold taken for requests and returned, with the balance after each.

The company name, VAT number and address on your invoices are edited in the billing portal, which Polar runs. Open it from Console > Billing. Receipts and invoices for every payment are under Billing > Payments.

Access

Who can read what

Every table in the database is protected by row-level security. The check runs inside the database on every read, not only in the console: a signed-in member can read only the rows of organizations they belong to, and some data needs a specific role.

DataWho can read it
Organization name, projects, members and their rolesEvery member
Members' names, emails and avatarsEvery member of an organization you share
API key list: name, first 12 characters, project, creator and datesEvery member
Usage, per request and per dayEvery member
Credit balance, tier and tier historyEvery member
InvitationsOwners and admins
Payments, receipts, invoices and credit historyOwners, admins and billing members
Model list and pricesAnyone

How changes are made

The console cannot write to these tables directly. Every change, such as creating or revoking a key, inviting a member, changing a role or setting a spend limit, goes through a database function that first checks your role in that organization and refuses the change if your role does not allow it. Payments are recorded only by the payment webhook, after Polar's signature on it is verified, and usage rows are written only by the API.

Roles in short: owners can do everything, including changing roles; admins manage projects, keys, members and billing; developers create and use keys, revoke their own and see usage; billing members manage credit and invoices and see usage; readers can only view. Organizations and projects has the full table.

Not used for training

Records and questions are processed in memory and never written to the database, so they are never used to train models. This holds for every tier, every mode and every endpoint. There is no setting to turn on or off.

Retention

DataHow long it is kept
Records, questions and answersNot kept. They exist only in memory while the request runs.
Request metadataWhile the organization exists. It is your usage history and the record behind every charge.
ProfileWhile your account exists.
MembershipsUntil you leave or are removed from the organization, or your account is deleted.
API keys (hash, first 12 characters, dates)While the organization exists, including after a key is revoked.
InvitationsWhile the organization exists, with the date each was accepted or revoked.
Payments and credit historyAs long as we need them for accounting, which can be after an account is deleted.

In plain terms: the content you send is gone when the response is sent. Everything else is kept for as long as the account or organization it belongs to, and billing records for as long as accounting requires.

Deleting your data

There is no delete button in the console. To delete your account, or to see or correct the personal data we hold about you, write to support@saanora.com from the email address you sign in with, and say what you want done.

When we delete your account, we remove your profile and your memberships. An organization's projects, usage and billing records belong to the organization, so they stay with it while it exists. Billing records we need for accounting may be kept.

Before you write, export what you want to keep: Console > Usage > Export CSV downloads one row per request, up to 100,000 rows, for the range (7, 30 or 90 days), project and model you selected. Records and answers from past requests cannot be returned or deleted, because they were never stored.

Some things you can do yourself at any time:

Keeping your own records

An audit log keyed by request id

Because the platform keeps no content, the request id is the link between your records and ours. Log it with each response. The example below appends one JSON line per request with the request id, the questions, the answers, the usage and any error. It stores a SHA-256 fingerprint of the record instead of the record, so you can match a record later without keeping a second copy. Store the full record instead if your audit needs it.

import hashlib
import json
import os
from datetime import datetime, timezone

import requests

API_URL = "https://api.saanora.com"
API_KEY = os.environ["SAANORA_API_KEY"]


def decide(state: dict, questions: dict, log_path: str = "decisions.jsonl") -> dict:
    """Ask Clara and keep your own audit record. Saanora does not store the record or the answers."""
    r = requests.post(
        f"{API_URL}/v1/decisions",
        headers={"Authorization": f"Bearer {API_KEY}"},
        json={"model": "clara-1", "state": state, "questions": questions},
        timeout=120,  # generous: the first request after idle can be slow
    )
    body = r.json()
    entry = {
        "at": datetime.now(timezone.utc).isoformat(),
        "request_id": r.headers.get("x-request-id"),
        "status": r.status_code,
        # a fingerprint lets you match a record later without keeping a second copy of it
        "state_sha256": hashlib.sha256(json.dumps(state, sort_keys=True).encode()).hexdigest(),
        "questions": questions,
        "answers": body.get("answers"),
        "usage": body.get("usage"),
        "error": body.get("error"),
    }
    with open(log_path, "a", encoding="utf-8") as f:
        f.write(json.dumps(entry) + "\n")
    if r.status_code != 200:
        err = body["error"]
        raise RuntimeError(f"{err['code']}: {err['message']} ({entry['request_id']})")
    return body["answers"]


answers = decide(
    {"message": "My order 48213 arrived broken. I would like my money back.", "order_status": "delivered"},
    {"wants_refund": {"type": "noul", "instructions": "Is the customer asking for a refund?"}},
)
print(answers["wants_refund"]["noul"])

The request_id in your log matches the request_id column in the usage export (for requests that reached the model) and the id of the decision (dec_ followed by the same characters). Errors carry it too, in the body and in the x-request-id header.

Send only what the decision needs

Clara answers from the record alone, so a field that no question depends on adds nothing to the answer. Leaving it out keeps personal data off the wire and lowers the cost, since you are billed on input tokens and every field in the record adds to them.

python
ticket = {
    "customer": "Dana Whitfield",
    "email": "dana@example.com",
    "phone": "+1 555 0100",
    "message": "My order 48213 arrived broken. I would like my money back.",
    "order_date": "2026-10-02",
    "order_status": "delivered",
}

# The questions are about the message and the order, so send only those fields.
NEEDED = ("message", "order_date", "order_status")
state = {k: ticket[k] for k in NEEDED}
Personal data in records. If your records contain personal data, you are responsible for having the right to send it and for telling people how you use it. See the acceptable use policy.

Policies and contact