API reference
Base URL https://api.canonopylabs.com. Every request and answer is JSON. The machine-readable contract is OpenAPI 3.1; after this first version it only changes additively (new endpoints, new optional fields).
Auth: Authorization: Bearer <key> (or X-API-Key: <key>) on everything except /health, /v1/plans, /v1/signup, /v1/login and /v1/demo/*. Domain paths accept the domain's name or its id.
Decide
Alias: POST /v1/systemone, same body.
modelstring<domain>@<version|latest>, likebank-support@latest. Names starting withjev-map to yourdefaultdomain. A domain that doesn't exist yet is created in day-one mode.statestring | object | arrayrequired- The case to decide.
questionsobject- Choice, Score and Noul questions in Jev's format. Leave out to ask every question the domain knows.
A Score with fewer than 2 levels is a 400 (Jev accepts it). Returns id, model (the version that answered), answers (each with source: trained, translated, fallback, rule or backup, and route: act or review), language (the language the text was detected in), decision (when the decision question was asked) and usage (decisions, plus input_tokens and output_tokens for Jev client compatibility; none of it is billed). See Questions and Decisions and rules.
One card per serving domain version: [{"name": "bank-support@3", "description": "…", "release_date": "2026-09-24"}].
Build from a Jev request
Alias: POST /v1/systemone/build, same body. Your own model from the same JSON you send Jev. No past decisions or answers needed; a few examples of what you send Jev make it better. See Start with no data.
statestring | objectrequired- ONE real example of the state your program sends. Each value is read as a field with a type and a
path; identifiers, empty values and list items past the first 8 aren't read. questionsobjectrequired- Your questions exactly as you send them to Jev (Choice, Noul, Score). Stored as they are.
examples(string | object)[]- Recommended: 5-20 more real states, each like
state(up to 50; states only, no answers). fieldsobject- What fields mean, keyed by a field's
pathor name: a sentence, or{"meaning", "unit", "range": [lowest, highest]}. E.g.{"order.amount": "the refund asked for, in USD (0-5,000)"}. rulesstring- Plain words. Rules that block answers of the decision question are enforced on every decision.
namestring- The model's name. Building again under the same name makes its next version.
descriptionstring- What it decides.
historyobject[]- Your past cases, up to 50,000: each
{"state", "answers"}or a flat row, as in an upload. Stored as one upload of the model. See Make it better with your history. interviewbooleantrue: a few questions about your JSON first; the build waits for your answers (statusawaiting_answers) up toanswer_timeout_seconds(default 600, at most 3600), then goes on with what it has.answer_timeout_secondsinteger- With
interview: how long it waits for answers.
model and anything else Jev takes are ignored, so a pasted Jev request works as is. An old sign_off field is accepted and ignored. Returns a Build at once:
build_id,domain,model(<name>@latest),kind(build,retrainorrule_change),previous_build(for a retrain or a rule change);status:awaiting_answers(withinterview: answer the questions),queued,building,training,checking,ready(serving),needs_attention(built, not serving: seequality) orfailed(seeerrors). Nothing waits for you in between (a build made before Oct 1 can still showawaiting_sign_off; it moves on by itself).phaseandphases(understanding,questions(withinterview),preparing,training,checking, each with alabeland astatus);messageandnext_stepin plain words;notes;understood:state(textorobject),fields(name,path,type),not_read,questions,decision_question,rules,rules_not_understood,rules_note,field_notes(path,meaning,unit,range) andfield_notes_not_matched(key,why) when you sentfields, andhistory(accepted,rejected,problems) when you sent past cases;examples: oncereadyorneeds_attention, about 20 examples of how your model decides, optional to review (the same asGET …/examples);qualityonce checked:bar(0.95),passed,situations,statement, and per questionagreement,passed,cases,advice(below the bar) andweakest(where,agreement,cases);own_cases(cases, per-questionagreement,statement) when your model was also measured on your own held-back cases;history:cases,learned_from,held_back,variations,flagged,pending,gaps(question,answer,your_cases,situations),statement;found_rules: rules found in your history, eachid,sentence,question,answer,support,agreement,statement,status(proposed,confirmed,rejected),hard,can_be_hard,changes(situations,of,statement);flagged:total,pendingand the first 20items(id,state,answers,rule,question,rule_answer,kind:foundorhard,status:pending,follow_rule,keepordrop,why);questions: the latest question round:round,status(open,answered,timed_out,closed),count,answered,waits,asked_at,answer_by,answered_at,waited_seconds,used(this_buildornext_retrain);retrain(a targeted retrain):since_version,added(disagreements,around_confusions,look_alikes,example_messages,corrections,from_play),areas(question,answer,area,before,after,cases),statement;rule_preview(a rule change):rules,previous_rules,situations,of,changes(question,from,to,situations), about 10examples(state,before,after),statement;stats(seconds,situations,examples,example_messages,download_bytes,model_file_bytes,version),version,errors,created_at,updated_at,finished_at.
400 (the example or the questions can't be read), 402 (a build is a training for your plan), 409 (the name belongs to a model not made this way, or a build of it is in progress), 429 (a build limit: 5 builds per workspace per UTC day, 30 a month with a subscription, 3 in all on the free trial, where it's a 402 once billing is on; see Build limits).
The Build. The second is a model's latest build (its first build, a rebuild, a retrain or a rule change); 404 for a model not made with POST /v1/build.
GET (round: 1 or 2, default the latest) → build_id, domain, round, status, questions (each id, kind, text, priority, refs (question, choices, fields), answer_with), answered, rounds, asked_at, answer_by, answered_at, waited_seconds, next_step. POST {"answers": [{"id": "q02", "text": "…"}, {"id": "q05", "unit": "dollars", "range": [0, 5000]}, {"id": "q07", "skip": true}], "done": true} → build_id, round, accepted, rejected (id, why), used (this_build, next_retrain or none), status, message. 400 (a key its question doesn't take, text over 600 characters). See Answer a few questions.
{"rules": [{"id": "fr_1", "decision": "confirm", "hard": true}, {"id": "fr_2", "decision": "reject"}]} → the Build. decision: confirm or reject; hard (with confirm, only when can_be_hard) also enforces it on every decision. Optional: nothing waits for it, and your decisions apply from the next retrain. 400 (an unknown id, or hard on a rule that can't be hard), 409 (the build is busy). See Rules found in your history.
GET: status (pending or reviewed; default all), limit (1-200, default 50), cursor → build_id, domain, total, pending, items (flagged past cases, as above), next_cursor. POST {"rows": [{"id": "h1042", "decision": "follow_rule"}]} or {"all": "drop"}: follow_rule (learn it with the rule's answer; not for a rule that only blocks answers), keep (learn it as it is) or drop (never learn it) → build_id, domain, reviewed, total, pending, message. Decisions apply when the model is next trained. See Flagged past cases.
Improve a model
→ domain, statement and items, most useful first: each kind (retrain_to_sharpen, rule_may_be_wrong, answer_these, upload_these, from_playtest, shadow_review, answer_questions), title, text, action (retrain, change_rules, answer, upload, review, none), and as they apply question, agreement, rule, contradicted, applied, decisions (decision_id, summary, answer, confidence). Read-only. See Improve your model.
For a model made with POST /v1/build: rules (plain words) and mode (add, the default, or replace) → a Build of kind rule_change, retrained with the new rules straight away. Its rule_preview says what changes, and its examples (the changed situations) are optional to review. The new version serves only if it passes the quality check. 400 (not a built model: use the set-up agent), 402, 409 (a build is in progress), 429. See Change a rule with a preview.
{"answers": {"action": "escalate"}} or {"action": "escalate", "note": "…"} → recorded, decision_id, domain, answers, message. Recorded as the decision's outcome (via: "marked_wrong"); the next retrain learns it. 400 (an unknown question or answer), 404. See Mark a decision wrong.
A targeted retrain is POST /v1/domains/{domain}/train on a built model (below).
Try it on real work
PUT {"on": true} (or false) → domain, on, since, version, cases, agreed, disagreed, to_review, reviewed, agreement, statement, next_step. See Shadow mode.
{"cases": [{"state": {…}, "current": {"action": "approve"}, "source": "jev", "confident": true}]} (up to 100; source: jev, process or person) → recorded, agreed, disagreed, items (index, id, agrees, error), message. Your model decides each one silently: not acted on, not billed, not in the decision log. 409 (shadow mode is off, or no trained version yet).
GET (at most 20) → domain, open, items (id, created_at, state, question, current, current_source, model, model_confidence, all_current, all_model), minutes, next_step. POST {"items": [{"id": "shd_…", "pick": "model"}, {"id": "shd_…", "answers": {"action": "escalate"}}, {"id": "shd_…", "skip": true}]} → reviewed, problems, open, message. Reviewed answers are learned at your next retrain.
This week's optional card → domain, setting (off, light, thorough), rate, title, open, minutes, items (decision_id, created_at, model, state, answers, decision), spot_checked (checked, agreed, accuracy, statement), next_step. Answer each with POST /v1/domains/{domain}/answers. The setting is PATCH /v1/settings {"spot_checks": "thorough"}.
{"model": "my-bot@3", "episodes": [{"score": 1240, "deaths": 1}], "situations": [ … ]} (1-1000 episodes of numbers; up to 300 optional situations) → id, domain, version, episodes, summary (per number: mean, min, max, episodes), situations_saved, learned, statement, next_step. GET (limit 1-50) → domain, runs. See Playtest your model.
Domains and set-up
namestringrequired- Lowercase, used in
model. descriptionstring- What it decides.
kind"decision" | "game"- Defaults to
decision.gameis coming soon. questionsobject- Questions in Jev's format.
decision_questionstring- The Choice question whose options are the actions.
rulesRule[]id,description,when(conditions on state fields),blockorallow_only.state_schemaobjectfields: eachname,type(number,category,text), optionalpath,values,min,max,description.graduationobjectmin_outcomesandauto.
Returns the Domain: everything above plus id, status (draft, ready, trained), actions, serving_version, latest_version, question_status (per question: trained or fallback, outcomes, needed), fallback, signoff, cases, created_at, updated_at, and safety_bar (read-only: target 0.97, a plain-words statement, strong_languages, and per question the threshold and automatic_share). A request with thresholds gets a 400: the bar sets itself.
PATCH takes any subset of description, questions, decision_question, rules, state_schema, graduation, and returns the Domain.
{"message": "…"} → reply, summary (the decision as set up so far, in plain words), domain, open_questions, next_step (describe: answer the open questions; train: it's set up, train it; follow_build: a rule change started; older set-ups can show review_examples or upload_history, both optional). On a model made with POST /v1/build, a message that changes the rules starts a rule change that retrains straight away, and the reply has its build_id.
Optional, any time: how your model decides on about 20 situations. GET → examples (each id, state, answers, why, described), status (ready, or preparing while the example messages of a decision made on text are being prepared: ask again in a minute), note and signoff (the review record; status none means not reviewed, which is fine). POST {"examples": [{"id", "ok", "correct", "note"}], "retrain": true} with only the examples you correct → the review record: status (signed_off or corrections_recorded), at, by, examples, corrections, and message. retrain: true starts the retrain in the same call: the answer has its build_id (a built model) or job_id (a described model). Without it, corrections apply at the next retrain. On a built model, corrections also go to your model's instructions, so the retrained model gives those answers.
Past cases, training, versions
Upload: a .csv or .jsonl file (multipart), a text/csv or application/x-ndjson body, or {"rows": [...]}. Query: mode (add, the default, or replace: these cases take the place of every uploaded case, in one step; nothing changes if no row is accepted) and file_name (names a raw body in the upload list). Returns accepted, rejected, total_cases, per_question, problems (first 20), upload_id, mode and replaced (cases, uploads). GET → total_cases, per_question, from_uploads, from_signoff, from_outcomes.
uploads → uploads (newest first, each id, source (upload, or signoff: examples you corrected), mode, file_name, created_at, cases, rejected; cases from before uploads had ids are the group earlier), total_cases, from_uploads, from_signoff, from_outcomes. Deleting one upload, or every uploaded case (confirm must be the domain's name; add signoff=true and outcomes=true to also delete the examples you corrected and the hosted decisions the next version would learn from), deletes permanently and returns removed (uploaded_cases, signoff_cases, decisions), uploads_removed, remaining and a plain-words message. Versions already trained are kept. Both answer 409 while a new version of the domain is being made, as does mode=replace. See Replace or remove cases.
Optional body: questions (only these), promote (auto, always, never), wait (default true). Returns a Job: id, domain, kind (train, option, graduation, multilingual, starter or build), status (queued, running, done, failed), version, message, report, and build_id for a model made with POST /v1/build. It learns from the cases the domain has now; the report's cases says how many, and how many earlier cases were removed since the last version. On a built model it's a targeted retrain: follow GET /v1/build/{build_id} for its retrain (what it added, and the before and after); with wait: true it waits for the build and answers with its training job.
Adds a Choice option or an action: question, option, description, optional when (plain words), never_when (conditions; adds a rule), also_allowed_under (ids of allow_only rules that should also allow the new action), promote, wait. Returns a Job whose report has before_after.
versions → serving_version and versions (each version, status, kind, created_at, questions, recommendation, size_bytes). promote {"version": 2} → {"serving_version": 2}; a roll-back is a promote of an older version. The report is described in Training and reports. progress → waiting (new cases since the latest version by source, differed, a plain statement, and training when a job is already running) and versions, newest first (each with cases, per-question accuracy, change_points, comparison: same_cases, own_held_back or not_comparable, starter, and live agreement with your reported outcomes from 20 of them). waiting.sources includes marked_wrong (decisions you marked wrong), and advice has the same items as GET …/advice. Read-only; see Improving.
application/zip with model.json, heads.onnx and rules.json. See Running it yourself.
The shared text reader a downloaded model uses, by id (reader-en-1, about 34 MB, or reader-multi-1, about 113 MB: files model.onnx and tokenizer.json). canonopy-runtime fetches it once and checks it against the model's recorded checksum.
Games coming soon
Game bots are coming soon; not yet available. Until they launch, these endpoints answer 503 with "type": "unavailable".
For kind: "game" domains. PUT {"code", "episodes", "max_steps"}: your game's Python, defining initial_state(seed), legal_moves(state), step(state, move) and optionally features(state, move). It runs in a sandbox and is never returned. Returns connected, moves, features, episodes, max_steps, checked_at. Game bots are download-first: /v1/decide on a game domain answers 400. To build a game player today, use a normal decision domain over the game state: see the Snake and Doom cookbooks.
Outcomes, unsure queue, day one
decision_id, answers (per question), action (shorthand for the decision question) → recorded, decision_id, graduation.
unsure → open and items (each decision_id, created_at, model, state, answers, decision, from_offline_device). answers {"decision_id", "answers"} → recorded.
Cases a downloaded model saved while it couldn't reach us; model.sync(), canonopy-runtime sync and canonopy sync send them for you. {"cases": [...]}, up to 100 per request (each as the runtime wrote it: id, created_at, model, state, questions, answers, decision) → accepted, already_there (sent before: nothing changed), rejected (each id and reason; they stay on the device), decision_ids and held. The held ones join the unsure queue with from_offline_device: true; outcomes take the runtime's id or the decision id. Answers from a device say source trained, rule or local_fallback (your own fallback). 402 after the trial with no subscription (the device keeps its cases), 409 for an archived model, 413 above 10 MB, 429 above 30 requests a minute.
Fallback: provider (typesafe, openai, local, none), api_key (write-only), base_url, model. Reads return has_key and key_last4.
Decision log
Newest first: items (each id, created_at, model, version, summary, question, answer, confidence, route, source, sources, language, has_outcome, from_offline_device, marked_wrong), next_cursor (pass it as cursor for older ones), filters and retention_days. Filters: since, until (ISO 8601; until is exclusive), source, route, answer=question:answer (repeatable), version, has_outcome, q (words in the case's text, or a decision id), and limit (1-200, default 50).
One decision in full: the fields above plus state, questions, answers, decision, rules_applied (your rules that blocked an option, each id, description, still_in_place, and chance for a rule on what the message is about), latency_ms and outcome (answers, reported_at, via: outcome, unsure_queue or marked_wrong).
The same filters, as CSV or JSONL (format=jsonl: one decision per line, in full), up to 100,000 decisions; X-Decisions-Exported and X-Decisions-Truncated say how many and whether there were more.
Delete one decision, or every decision before a date (confirm is the model's name) → deleted, learning_cases_deleted, message. Permanent. 409 while a new version is being made if it would change what that version learns from. All of these keep working after the free trial ends. See The decision log.
Account
org, user, plan, and usage: decisions_this_month, active_domains, monthly_price_usd (always $20: the price is flat per workspace).
POST {"name": "production"} returns the new key once, in key; we keep only a hash.
decision_retention: how long decisions are kept, 30_days, 90_days (the default), 365_days or until_deleted. Reads also return decision_retention_days and older_than_retention (deleted at the next daily clean-up). Older decisions are deleted once a day, including those with outcomes: later versions no longer learn from them.
[{"name", "price_usd_per_month": 20, "billing", "included", "rate_limit_per_second": 50}]: $20 a month per workspace, flat, with unlimited decision models, decisions and retraining and up to 30 builds a month. (price_usd_per_domain_month is the same number, kept for older clients.) No key needed.
Sign-up (org_name, email, password; only when open sign-up is on) returns your first API key once. Login returns a 12-hour console session token, used as a Bearer key.
Starts the subscription: one flat price, $20 a month for the workspace, however many decision models you have. Answers 503 unavailable until billing is switched on.
Your free trial and subscription: trial (state: not_started until your first trained model or first decision, then active for 5 days with ends_at and days_left, then ended), trial_ends_at, status, can_train, can_decide, can_download (always true), grace_until after a failed payment, and message when something is paused. After the trial with no active subscription, training, new options and /v1/decide answer 402 payment_required; downloads never do.
Open endpoints
The demo decides with the bank-support model only, 30 requests per minute per IP. /health → {"ok": true, "version": "0.1.0"}.