API reference
Base URL: https://api.jepela.com (the API of your key's region; see Regions). Every /v1 route except sign-up needs Authorization: Bearer <key>, and every POST Content-Type: application/json. A POST body may be compressed with Content-Encoding: gzip (or x-gzip) or zstd, one layer, as TypeSafe's newer SDKs send it. Bodies are capped at 1 MB (16 MB for /v1/batches and /v1/golden); a compressed body counts once decompressed. Duplicate JSON keys are rejected, and a body nested more than 100 levels deep (arrays and objects; in a JSON Lines batch, each line) is refused with a 400. 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 jepela-ft-*; default jepela |
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), names (default true: the subject's name and aliases are searched with the state), question_words (default true: the questions' instructions are searched too), focus (default false: keep only lines that share a word with the state beyond those names), explain (default false: per memory line, the answer's move when that line alone is left out; one engine pass per line, at most 8). meaning is internal, not a supported option: it is accepted as true or false and is off unless sent, but it may change or be refused without notice, so do not rely on it |
quality | "fast" (default), "balanced" or "best" | speed or accuracy: above fast, the answers the engine is least sure of, those whose state it could not read in full and every answer of a decision with memory or computed facts are answered again by a larger, slower second tier; best sends more of them (Request options) |
method | "auto" (default), "engine" or "match" | how choice questions are answered: auto matches choices with more than 20 options by vectors and asks the engine the rest; engine reads every option; match matches every choice (Choice) |
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 matched (or, with method: "engine", answered in two rounds) 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, read before a field of the state with the same name; 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: from the same engine build, at most 24 hours old, removed by POST /v1/decisions/delete; a repeat is answered while the engine is briefly away (Request options) |
Questions: {"type": "choice", "instructions", "criteria": {name: description}} with 1 to 255 options (more than 20 are matched by default; see method); {"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 question whose depends_on is set to anything but null or an empty value is refused with a 422: every question is answered on its own, so ask the questions together and apply the condition in your code. A request may need at most 400 engine questions, counting every option order, every group of a two-round choice and its final round; matched choices are not engine questions. context is not accepted: Jepela builds it from the subject's memory and derive.
Fields Jepela does not read: assets and images (pictures, which Jev takes), in the request or in a question, are refused with a 422 when they hold anything, before anything is asked or charged, because Jepela reads text only and an answer from the text alone would look as if the pictures had been seen; describe what they show in the state. Empty ones pass. Any other field Jepela does not read, at the top of the request, in memory or in a question, is answered as usual, without that field, and the answer carries an unknown_field warning that names it and the closest field Jepela reads (a question's warning has question): a misspelt subjet gives "unknown field subjet; did you mean subject?" on a decision made without memory, and memory.comapre gives no comparison and "did you mean memory.compare?"; a noul's critera is a question without its descriptions and "did you mean questions.refund.criteria?". A question's fields are those TypeSafe's API reference documents (type, instructions, criteria; and depends_on, above), so code written for Jev keeps working, and a typo is seen. A request refused for a missing field names the misspelt one: "choice needs 1 to 255 named candidates (unknown field questions.route.criterias; did you mean questions.route.criteria?)". A name that looks like a key or a token is never repeated in a warning. A request with an unknown field is billed like the same request without it.
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 (the engine's metadata: version, the Jepela build id, which is the hash of the engine's code and changes whenever the engine does; device; timings) |
answers | per question, each with type, confidence (1 minus the normalised entropy of the probabilities for a choice or score; the larger of yes and no for a noul), answer_confidence (the probability of the answer given, on one scale for every type: the chosen option's, the most likely level's, or the larger of yes and no; always the top value of the answer's own probabilities, after a tenant temperature too), method (engine or match; matched answers have similarity) and, on an answer the engine read (method engine), the engine's action (not on matched answers or on answers the second tier gave, nor in their first; do not decide on it; see below the table). 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. With quality balanced or best: right_probability (the estimate that the engine's answer is right), and on an answer the second tier gave tier (2) and first (the engine's own answer) |
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; chars_read counts from the start, or for an array, which is cut from its start, the last characters; with windows, read and chars_read count the tokens and characters of the state the windows covered: from its start, or for an array too long for the windows, which are then placed at its end, up to its end); context_lines_read when such lines were placed; turns_dropped when Jepela left out that many of an array's oldest elements so that the lines placed before it were read (then truncated is true, and read and chars_read count what was sent, the lines and the newest elements, read whole); windows when windows read the whole state; for a matched choice, what the matching model read, with method: "match" and counted with its tokens (see Choice) |
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: the first ones, or in an array, which is cut from its start, the last ones), lines_dropped (left out of its 64 context lines), hits and rules_fired (each line with read: whether the engine read it, as many true as lines_seen), rules_broken (rules not checked: id, error, and missing, the field, when the request had no field the rule reads), recall_ms, without_memory and changes (with compare, otherwise null: the same call without the memory and its rules, on the same elements of an array when Jepela left out its oldest, and per question what changed; a choice's entry has excluded when exclusion rules removed options), placebo (with placebo: lines, answers, content_effect, memory_effect, memory_distance, placebo_distance, presence_only; where the second tier gave the answer, measured_on_first lists the questions whose effects are measured on the engine's own answer first: see Memory); when they apply: pinned (ids of pinned rules sent), excluded (per question, the options removed by exclusion rules and why), exclusions_skipped, explain (a list of {line, kind, read, effect}; where the second tier gave the answer, measured_on_first lists the questions whose effect is the move of the engine's own answer first: see Memory) |
quality | with quality: level; above fast also below (the right_probability under which an answer is answered again), asked_again (question ids), why (per question: unsure, cut or memory) and, when the second tier gave no answer in time or failed, fallback (questions that kept the engine's own answer, marked tier: 1, and the reason) |
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 (the engine's count, what you pay for), output_tokens (always 0), state_tokens, state_tokens_read, state_truncated (over the questions the engine read; a matched choice's own reading stays in reading), memory_lines_used, memory_lines_seen, match_questions (when some were matched), tier2_questions and tier2_input_tokens (when the second tier answered), cached, engine_ms, gateway_ms, cost, currency, balance. answers_billed is the sum over questions of the option orders used (1 without robust) × windows; the compare, explain and placebo passes are not billed |
action is {"act_probability": <number>}, an output of the engine that Jepela passes through as it comes and reads nothing from; it is not one of the answer's probabilities. On all 40 answers recorded on 2026-09-23 (Examples) it was 1.0: for choice, score and noul questions alike, with and without memory, at confidences from 0.016 to 1.0. So it tells you nothing about an answer: do not decide on it. Decide from probabilities and confidence (Confidence), or, once you have labels, from the thresholds GET /v1/quality gives in automate_at (Quality).
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 (the lines it did not hold yet: a line the subject holds is not stored again), lines_stored, warnings, usage |
POST /v1/memory/forget | subject, concept, preview (true: nothing is removed and the answer says what would go, with "preview": true, no usage and no verified_forgotten; not billed, no ledger line) | lines_removed, removed_lines (their text), rules_removed, removed_rules (id and text of each), matched_unsure (lines and rules removed although the case does not show the name there: the first word of a sentence, a list item or a table cell, a word or a heading in capitals or title case, or a name of several parts in another case; or a word of the concept matched only with other accents or none, "José" for "Jose", "Jose" for "José", or with a letter such as ø, æ or ł written without it, "Søren" for "Soren", "Kjaer" for "Kjær"), related (lines and rules kept that may be about a name: they hold the concept with a name in another case, "Billing is per month." for "Per", or a part of a name of several parts, "Ann prefers email." for "Ann Lindqvist"), verified_forgotten, leaks, usage, warnings; a line or rule goes when it holds every content word of the concept as a whole word, in any script ("Ann" is not in "annual"; "MÜLLER" is "Müller"): a word written in lowercase with case ignored, also as a part of a hyphenated word that holds a letter ("dairy" in "dairy-free", not "2026" in "2026-10-16"), a word written with a capital letter (a name) only where the line has a capital too ("Per" is not in "per month"), and also in its possessive and genitive forms ("Ann's", "Åsas") and as a part of a hyphenated name ("Smith" in "Smith-Jones"), with the apostrophes inside it, however typed ("O'Brien" is "OʼBrien", not "Brien") |
POST /v1/memory/delete | subject | lines_removed (the lines the subject held), batch_requests_deleted (the subject's requests taken out of your batches, with their results), usage, warnings; the lines and rules on disk go too. Any other field with a value is refused (422) and nothing is deleted |
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; optional with pin or exclude, then always), text (at most 500), pin (true: the line is sent at every decision, first), exclude (1 to 20 option keys removed from choice questions in code when the rule holds; its text is never sent to the engine), question (limit an exclusion to one question id; refused without exclude), 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, warnings; billed as one memory line |
POST /v1/memory/aliases | subject, aliases (at most 20 names, 1 to 80 characters each; an empty list clears them) | aliases, searched_as (the words recall adds to the state), warnings |
POST /v1/memory/aliases/delete | subject, aliases (the names to take out, at most 20, matched in any case; required, an empty list removes nothing) | removed (the aliases that went, as stored), not_found (the names the subject did not hold), aliases (those left), searched_as, warnings; adds no name; not billed |
GET /v1/memory/rules | subject | rules |
POST /v1/memory/rules/delete | subject, rule_id | deleted, rules, warnings |
Fields a memory route does not read get the policy of a decision: the request is carried out without the field, and warnings has an unknown_field warning naming it and the closest field the route reads, or the route that reads it: a rule with ttl_secs is stored without an expiry and "unknown field ttl_secs; did you mean ttl_seconds?" says so; text sent to forget is "read by POST /v1/memory/remember and POST /v1/memory/rules, not here". A field that would change what is done and cannot be honoured is refused with a 422 before anything is stored or deleted, and nothing is charged: remember with when, pin, exclude, question, expires or ttl_seconds (or a name close to one: a remembered line is recalled by its words, always and until forgotten; send it as a rule), delete with any field but subject (it deletes everything about the subject, more than such a field asks for), forget with a name close to preview in any case or spelling (Preview, PREVIEW, preview_only), dry_run, dry run, simulate, simulation, what_if or noop, or an object that holds one ("options": {"preview": true}), or with any query string (POST /v1/memory/forget?preview=true; the caller asked only to look; send "preview": true in the body), and a rule's question without exclude. Every route that deletes reads only its JSON body and has no preview but forget's: a query string on forget, delete, rules/delete, /v1/golden/delete, /v1/decisions/delete or /v1/finetune/delete (?dry_run=true) is refused with a 422, and so is a field that asks rules/delete, /v1/golden/delete or /v1/finetune/delete only to look (a name like preview or dry_run, as for forget, or an object holding one); nothing is deleted or charged. A field whose value is null, false or empty asks for nothing and is only named. customer, the older name of subject, is read everywhere. A 422 for a missing field names the misspelt one the body carried: "concept must not be empty (unknown field concpt; did you mean concept?)".
Removing data never waits for money: forget (and its preview), delete, rules/delete and aliases/delete work when the balance is zero or below, and while the account is suspended (a deletion request suspends it until it is erased), as do the golden, decision and fine-tuned model deletes below. A suspended account can also read its data on every GET route (GET /v1/memory with lines=true, the rules, golden cases, batches, fine-tuning jobs and usage), so it can still answer a data subject's request for access; it cannot store, change or decide anything. An erased account or a revoked key can do none of this. A forget or delete is charged by the prices while the balance covers it; when it does not, it is carried out all the same, its ledger line says cost 0, and usage.unbilled says what it would have cost, so removing data never takes the balance below zero.
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, model and tier (2 for the second tier's answers): 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?, method?}; id is 1 to 64 characters, and a case with an existing id replaces it; customer, the older name, is stored as subject (subject wins when both are sent); context, assets and images are refused as in a decision | added, cases (at most 5,000 per tenant), warnings (an unknown_field warning, with case, for each field a case is not kept with) |
GET /v1/golden | count, cases (ids, questions, expected), runs | |
POST /v1/golden/run | model (default jepela) | run_id, cases, accuracy, questions, changed_since_last_run (null on a model's first run), failed (each case that failed or was not run, with the reason); billed like decisions; a case that fails, or is not run because the credit ran out, is left out of the accuracy and not billed |
POST /v1/golden/delete | {"ids": [...]}, or {} for every case (an empty list deletes nothing); without ids, any other field with a value is refused (422) and nothing is deleted | deleted, warnings |
POST /v1/finetune | base (english default, multilingual, typed-decisions); trains on your golden cases, at least 20 | the job: job, model, cases, held_out, state; 409 while another of your jobs runs or the engine is off. See Fine-tuning |
GET /v1/finetune | jobs, min_cases, bases (the bases the engine serves now) | |
GET /v1/finetune/<job> | state (running, saving, done, failed with error), result (won, base_accuracy, tuned_accuracy, held_out_questions, log losses, training_seconds), usable | |
POST /v1/finetune/delete | model | deleted; its files and its name are gone |
POST /v1/decisions/delete | {}; a field with a value is refused (422) and nothing is deleted | decisions_deleted (question records), labels_deleted, batches_deleted, warnings: every stored decision record and label of the tenant, and every batch with its requests and results (a running one stops); the answers the cache keeps for the tenant go too |
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, then deleted automatically. Batches are kept 30 days from the upload, then deleted with their requests and results. Answers kept by cache (probabilities and option names, in the gateway's memory) are served for at most 24 hours.
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, stopped: account suspended, stopped: key revoked, or failed with error when the job itself broke), total, done, succeeded, failed, cost, expires (when it is deleted), removed (requests taken out when their subject's memory was deleted) | |
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: runtime, "engine" and the Jepela build id, and the loaded checkpoints; this tenant's fine-tuned jepela-ft-* models also carry fine_tuned: true), default, aliases |
GET /v1/usage | credited, spent, debited (credit taken back by refunds and disputes), balance, requests, rate_limit_per_minute, prices, totals, recent (last 20 requests), 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, the region, and the engine's code hash; nothing about tenants or subjects), GET /v1/regions (the regions, their API addresses and status; see Regions), GET / (a how-to), GET /playground, GET /docs, GET /docs/<page>.md, GET /llms.txt, GET /robots.txt (crawlers may read these pages; the /v1 routes need a key).
Errors
{"error": {"type": "...", "message": "..."}}
| Status | Type | When |
|---|---|---|
| 400 | invalid_request_error | a Content-Length that is not a number, a compressed body that does not decompress (or holds more than 64 gzip members), or JSON nested more than 100 levels deep |
| 401 | authentication_error | no key, unknown key, revoked key |
| 402 | payment_required | the tenant's balance is zero or below; never on a route that removes data: POST /v1/memory/forget (and its preview), /v1/memory/delete, /v1/memory/rules/delete, /v1/memory/aliases/delete, /v1/golden/delete, /v1/decisions/delete and /v1/finetune/delete |
| 403 | signup_disabled, invite_required, account_suspended | sign-up is off, or needs an invite code; or the account is suspended (a suspended account may still read its data on every GET route and call the routes that remove data, listed under 402) or erased |
| 404 | not_found | unknown route, page, batch, or a decision id of another tenant |
| 421 | wrong_region | the key (or the sign-up) belongs to another region; the message and the Location header give its API address |
| 409 | conflict | a limit: 5,000 golden cases for a tenant, 10,000 memory lines or 100 rules for a subject, nothing was stored; or a fine-tuning job that cannot start now (another of yours runs, or the engine is off) |
| 413 | invalid_request_error | body over the cap (a compressed body counts once decompressed) |
| 415 | invalid_request_error | not application/json (or JSON Lines for batches); or a Content-Encoding other than gzip, x-gzip or zstd, or more than one |
| 422 | invalid_request_error | the reason is in the message: bad question, unknown model (a Jev name such as jev-latest is told to set the model to jepela), bad subject id, a derive or when expression that is not allowed |
| 429 | rate_limit_error | over the account's requests per minute (all its keys together), 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 memory service behind the gateway is down or answered an error; when the engine is switched off the message says the engine is off. 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.