# API reference

Base URL: `http://<host>:8797`. Every `/v1` route except sign-up needs `Authorization: Bearer <key>`, and every POST `Content-Type: application/json`. Bodies are capped at 1 MB (16 MB for `/v1/batches` and `/v1/golden`). Duplicate JSON keys are rejected. Connections stay open between requests (HTTP/1.1).

## POST /v1/systemone

Request:

| Field | Type | Meaning |
|---|---|---|
| `state` | string, object or array (required) | what to evaluate; see State for how much of it the engine reads |
| `questions` | object of question id to question (required) | at least one |
| `model` | string | a name from `/v1/models`, or this tenant's fine-tuned `jers-ft-*`; default `jers` |
| `subject` | string, 1 to 36 of `A-Za-z0-9_-` | use this subject's memory and rules |
| `memory` | object | `use` (default true with a subject), `top_k` (1 to 20, default 6), `min_share` (0.05 to 1, default 0.2), `compare` (default false), `placebo` (default false) |
| `robust` | `true` or `{"orders": 1-5}` | ask each choice of 2 to 20 options in several option orders and average them; `true` is 3. A choice of more than 20 options is answered in two rounds instead and is not reordered |
| `windows` | `true` or `{"combine": {"question_id": "max" \| "min" \| "mean"}}` | read a state that is cut in overlapping windows (32 tokens shared), at most 16 |
| `derive` | object, at most 20 names | facts computed in code: `{"name": "expression"}` or `{"name": {"when": "expression", "fact": "text", "else": "text"}}`; a name starts with a letter and has at most 40 letters, digits or underscores; an expression has at most 300 characters, a fact at most 500 |
| `values` | object, at most 50 fields | numbers and text that `derive` and rules may use; `today` sets the date `days_since` counts from |
| `cache` | boolean | an identical request that does not use a subject's memory returns the stored answers |

Questions: `{"type": "choice", "instructions", "criteria": {name: description}}` with 1 to 255 options (more than 20 are answered in two rounds); `{"type": "score", "instructions", "criteria": [level, ...]}` with 2 to 10 levels, lowest first; `{"type": "noul", "instructions", "criteria"?: {"true", "false"}}`. `instructions` and descriptions may be strings, objects or arrays. A request may need at most 400 engine questions, counting every option order, every group of a two-round choice and its final round. `context` is not accepted: Jers builds it from the subject's memory and `derive`.

Response:

| Field | Meaning |
|---|---|
| `decision_id` | `dec_` and 24 hex digits; send it back with `/v1/feedback`. The state is never stored |
| `model` | the name asked for, aliases resolved |
| `engine` | `name`, `checkpoint` (the one that answered), `runtime` |
| `answers` | per question, each with `type`, `confidence` and the engine's `action`. choice: `choice`, `probabilities`; `rounds` and `groups` above 20 options; `orders` and `agreement` with robust. score: `score` (probability-weighted level), `probabilities`, `legend`. noul: `noul` (probability of yes). With a tenant temperature: `calibration` and the `raw_` values before it. With windows: `window` |
| `warnings` | known traps in this request: `{code, message, question?}`; see Warnings |
| `reading` | per question: `state_tokens`, `read`, `room`, `chars_read`, `truncated` (counted on the state with memory and computed lines in front); `context_lines_read` when such lines were placed; `windows` when windows read the whole state |
| `derived` | the value of every `derive` name |
| `memory` | with a subject: `subject`, `lines_used` (fired rules and recalled lines sent to the engine), `lines_seen` (how many of them it read), `lines_dropped` (left out of its 64 context lines), `hits`, `rules_fired`, `rules_broken`, `recall_ms`, `without_memory` and `changes` (with compare, otherwise null), `placebo` (with placebo) |
| `windows` | when the state was read in windows: `count`, `covered_all` (whether the windows reached the end of the state), `state_tokens`, `tokens_per_window`; each answer's `window` names the window it came from (null with `mean`) |
| `usage` | `questions`, `answers`, `answers_billed`, `engine_answers`, `input_characters`, `input_tokens` (when the engine counted them), `state_tokens`, `state_tokens_read`, `state_truncated`, `memory_lines_used`, `memory_lines_seen`, `cached`, `engine_ms`, `gateway_ms`, `cost`, `currency`, `balance`. `answers_billed` is the sum over questions of the option orders used (1 without robust) × windows × passes (2 with placebo; compare is free) |

## Memory

| Route | Body or query | Response |
|---|---|---|
| `POST /v1/memory/remember` | `subject`, `text` (one fact per line, each at most 2,000 characters; a subject holds at most 10,000 lines) | `lines_added`, `lines_stored`, `warnings`, `usage` |
| `POST /v1/memory/forget` | `subject`, `concept` | `lines_removed`, `rules_removed`, `verified_forgotten`, `leaks`, `usage` |
| `POST /v1/memory/delete` | `subject` | `lines_removed` (lines that were in RAM), `usage`; the lines and rules on disk go too |
| `GET /v1/memory` | `subject`, `lines=true` | `lines_stored`, `rules`; with `lines=true` also `lines` and `rule_list` (`id`, `when`, `text`, `expires_at`), word for word |
| `POST /v1/memory/rules` | `subject`, `when` (at most 300 characters), `text` (at most 500), `expires` (ISO date or time) or `ttl_seconds` (at most ten years); at most 100 rules per subject | `rule` (with `expires_at`), `rules` (how many), `usage`; billed as one memory line |
| `GET /v1/memory/rules` | `subject` | `rules` |
| `POST /v1/memory/rules/delete` | `subject`, `rule_id` | `deleted`, `rules` |

## Labels and quality

| Route | Body or query | Response |
|---|---|---|
| `POST /v1/feedback` | `decision_id`, `question`, `correct` (an option, a level, or true/false) | `label`, `correct`, `labels_for_this_question`; `calibration_refit` at 50, 100, 200 … labels |
| `GET /v1/quality` | `question`, `days` (default 30), `model`, `source` (`feedback` or `golden`) | per question and model: `labels`, `accuracy`, `calibration_error`, `brier`, `log_loss`, `bands`, `automate_at` |
| `POST /v1/calibration/fit` | `model`, `question`, `source`, `min_labels` (at least 20, default 50) | `results`: per question, and per group of questions with the same type and number of options, the fitted temperature and whether it was kept |
| `GET /v1/calibration` | | `temperatures` in use |
| `POST /v1/golden` | `cases`: `{id, state, questions, expected, subject?, values?, derive?, memory?, robust?, windows?}`; `id` is 1 to 64 characters, and a case with an existing id replaces it | `added`, `cases` (at most 5,000 per tenant) |
| `GET /v1/golden` | | `count`, `cases` (ids, questions, expected), `runs` |
| `POST /v1/golden/run` | `model` (default `jers`) | `run_id`, `cases`, `accuracy`, `questions`, `changed_since_last_run` (null on a model's first run); billed like decisions; a case that fails, or is not run because the credit ran out, is left out |
| `POST /v1/golden/delete` | `{"ids": [...]}`, or `{}` for every case (an empty list deletes nothing) | `deleted` |
| `POST /v1/decisions/delete` | `{}` | `decisions_deleted` (question records), `labels_deleted`: every stored decision record and label of the tenant |

Decision records (the decision id, question ids and a hash of each question, the probabilities before and after calibration; never the state or the question text) are kept 30 days.

## Batches

| Route | Body | Response |
|---|---|---|
| `POST /v1/batches` | `{"requests": [...]}`, or JSON Lines with `Content-Type: application/x-ndjson`; each request a `/v1/systemone` body with an optional `custom_id`; at most 5,000 | the job: `id`, `status`, `total` |
| `GET /v1/batches` | | `batches` |
| `GET /v1/batches/<id>` | | `status` (`queued`, `running`, `done`, `stopped: no credit left`, or `failed` with `error` when the job itself broke), `total`, `done`, `succeeded`, `failed`, `cost` |
| `GET /v1/batches/<id>/results` | | JSON Lines: `custom_id`, `status`, `response` or `error`, in the order sent |

## Account

| Route | Response |
|---|---|
| `GET /v1/models` | `models` (name, engine, checkpoint, description, available, pinned; this tenant's fine-tuned `jers-ft-*` models also carry `fine_tuned: true`), `default`, `aliases` |
| `GET /v1/usage` | `credited`, `spent`, `balance`, `requests`, `rate_limit_per_minute`, `prices`, `totals`, `recent` (last 20 ledger lines), `keys` |
| `POST /v1/signup` (no key) | `email`, `invite_code` when the gateway asks for one → `tenant`, `key` (shown once), `key_id`, `credit`, `currency`, `note`. Off unless the owner turns it on; by default 3 sign-ups per address a day, and the new key gets 60 requests per minute |

## No key needed

`GET /health` (whether the gateway, the memory service and the engine are up, and the engine's code hash; nothing about tenants or subjects), `GET /` (a how-to), `GET /playground`, `GET /docs`, `GET /docs/<page>.md`, `GET /llms.txt`.

## Errors

`{"error": {"type": "...", "message": "..."}}`

| Status | Type | When |
|---|---|---|
| 400 | `invalid_request_error` | a Content-Length that is not a number |
| 401 | `authentication_error` | no key, unknown key, revoked key |
| 402 | `payment_required` | the tenant's balance is zero or below |
| 403 | `signup_disabled`, `invite_required` | sign-up is off, or needs an invite code |
| 404 | `not_found` | unknown route, page, batch, or a decision id of another tenant |
| 409 | `conflict` | a limit: 5,000 golden cases for a tenant, 10,000 memory lines or 100 rules for a subject; nothing was stored |
| 413 | `invalid_request_error` | body over the cap |
| 415 | `invalid_request_error` | not `application/json` (or JSON Lines for batches) |
| 422 | `invalid_request_error` | the reason is in the message: bad question, unknown model, bad subject id, a `derive` or `when` expression that is not allowed |
| 429 | `rate_limit_error` | over the key's requests per minute, or too many sign-ups; `Retry-After` in seconds |
| 500 | `server_error` | the gateway failed; the message names only the error type, and nothing was charged |
| 502 | `engine_error` | the engine or the product behind the gateway is down or answered an error; nothing was charged (a proxy in front of the gateway may answer 503 for the same) |

Retry 429 after `Retry-After`, and 502 or 503 with backoff. The SDKs do both, and never send a decision again once it may have arrived.
