API Reference
The platform provides a REST API for all operations. Full endpoint documentation is generated from the OpenAPI specification.
For browsing the documented API spec:
- Swagger UI — Interactive endpoint explorer with request/response schemas
- ReDoc — Alternative API reference in a clean reading format
For the live spec from your running deployment:
/api/docs— Available on your platform host; always matches the version you're running
The static views above are pinned to the
openapi.jsonshipped with this docs site. The live view reflects whatever code is actually deployed on your platform.
A write request naming a field the endpoint does not have is refused
Endpoints that create or change something answer 422 and name the
offending field rather than succeeding and ignoring it. A misspelling is
the case this is for: porject_id on a duplicate would once have created a
copy belonging to no project, and streaming_atribution on a run would
have returned a completed run with no attribution payload — both reported
as success, and both discoverable only by reading the response back
field-by-field. The error body carries "type": "extra_forbidden" and a
loc naming the field, so a client can report which key was wrong.
Read endpoints are unaffected, and so are the response bodies: a field the platform adds to a response never breaks a client.
Documents API
The platform exposes REST endpoints for Document management. These are fully documented in the Swagger UI and ReDoc references linked above. Key endpoints:
-
GET /documents — List Documents, optionally filtered by project and/or document type.
Query parameters:
project_id(optional, string) — restrict results to Documents in the given projectinclude_deleted(optional, boolean, defaultfalse) — include soft-deleted Documentstypes(optional, string) — comma-separated list of Document types to include. Allowed values:gold,silver,bronze, or the sentinelall.- When omitted or empty, the endpoint returns
goldandsilverDocuments only (bronze excluded). types=bronzereturns only bronze Documents.types=gold,silverreturns gold and silver (equivalent to the default).types=allreturns every Document type including bronze.
- When omitted or empty, the endpoint returns
Example requests:
# New default: gold + silver only (no bronze on the wire) GET /documents?project_id=proj-1 # Bronze Documents only (for the Documents tab's lazy bronze load) GET /documents?project_id=proj-1&types=bronze # Legacy-equivalent: gold + silver + bronze GET /documents?project_id=proj-1&types=allBreaking change (Phase 38.3)
Prior to this release,
GET /documentswith notypesparameter returned all Document types including bronze. As of Phase 38.3, the default excludes bronze so that long-running online-learning runs (which accumulate thousands of bronze Documents) do not slow the primary Documents listing. API consumers that need the legacy behavior must passtypes=allexplicitly.
See Documents for conceptual documentation on the Document lifecycle.
Rules API
The platform exposes REST endpoints for authoring and validating the DSAIL code in a rule. These are fully documented in the Swagger UI and ReDoc references linked above.
Some constructs are rejected at compile time
DSAIL validation enforces a hard boundary that API consumers should be
aware of: a collection (set/array) must have compile-time-known
membership, so a set or array whose members would come from document
extraction is rejected rather than accepted and evaluated. To reason
about a value the document supplies more than once, quantify over a scalar
claim instead of collecting it. See the DSAIL reference —
Collections and
Multiple Answers (multi-instance) —
for the full list of what compiles and what does not, so a POST of rule
text that fails validation is not a surprise.
Registry claims are named with an @ sigil
Rule DSAIL posted through these endpoints may name a claim from the shared
corpus registry as @namespace.name — in its
declaration and at every reference. A bare identifier is always local to
the rule, so existing rule text is unaffected and continues to mean
exactly what it always meant. declare local @ns.name is rejected: local
means private to this rule, which is the opposite of registry-backed. See
Naming a registry claim.
A claim payload for a registry claim reports the qualified name; a rule-local claim reports the bare one. The corpus objects those claims are defined in have their own surface — see the Corpus API.
Claim fields in rule/claim responses. Alongside the legacy datatype
field, claim payloads carry the richer DSAIL typing that the compiler
derives from the declaration:
| Field | Description |
|---|---|
dsl_type |
The declared DSAIL type — e.g. "enum", "numeric", "Bool.Tri". |
boolean_subtype |
For boolean claims, the subtype: Tri, Monotone, Paraconsistent, or Scoped (see Boolean subtypes). Absent for non-boolean claims. |
ordered |
For enum claims with a declared domain, true when declared with the ordered [...] form, false for the unordered {...} form. Ordered is what makes the claim comparable: <, <=, > and >= are accepted between it and a quoted label of its own domain and follow the declaration order, while the unordered form takes ==, != and membership only. See Enums. |
datatype |
The legacy coarse field (bool / numeric / symbol), retained for backward compatibility. |
extraction_status |
How the claim gets its value: extracted (a question is asked of the document — for a registry claim, the corpus holds the wording), solved_for (corpus rules determine it, so it has no question by design and is never asked), incomplete (nothing gives it a value — no question and no rule determining it), unresolved (a registry claim the corpus does not hold at the pinned version). Present on every claim. A blank question means something different in each case, so read this before deciding what to do about one. |
question_source |
corpus for a registry claim — the wording is the shared vocabulary's, returned for display and not editable here — or local for a rule-local claim. |
claim_version |
For a registry (@ns.name) claim, the corpus version this reference is pinned to. Absent means unpinned. Accepted on claim create and update as well; on update, sending null unpins the reference, while omitting the field leaves the pin as it is. |
A claim the rule's DSAIL does not declare is refused
POST /api/v1/rulesets/{ruleset_id}/rules/{rule_id}/claims answers 400
when the key is not among the rule's declare statements, naming the keys
that are. No assertion can read such a claim, so it would be asked of every
document for a value nothing uses. A rule whose DSAIL does not compile is
exempt — there is no declared set to check against, and refusing every claim
on a broken rule would block the author from fixing it. Run configuration
applies the same rule from the other side: an undeclared claim row that
reached a ruleset by some earlier route is skipped rather than asked.
A registry claim's question cannot be reworded through a rule
PUT /api/v1/rulesets/{rs}/rules/{rule}/claims/{claim} answers 409 when
the body carries a question that differs from the one the shared
vocabulary holds for a @ns.name claim. One claim has one question, so the
wording is the corpus's and there is nowhere in a rule to keep a different
one — the alternative was accepting the request, discarding the field, and
answering 200, which a caller could only detect by diffing the echoed body.
Two things stay accepted. Sending back the wording the endpoint just
returned is not a reword, so the ordinary read-modify-write cycle works.
And a blank or omitted question is accepted, because that is what the
row stores for a registry claim anyway. To give a rule a question of its
own, give it a claim of its own: a bare identifier the registry does not
hold is always rule-local. Reword a registry claim in Corpus →
Vocabulary.
Readiness. GET /api/v1/rulesets/{ruleset_id}/readiness returns
{ready, warnings[]}, optionally scoped to one rule with rule_id. Each warning
carries code, rule_id, rule_name, claim_name and a message:
no_rules, no_dsl, compile_error, no_claims, empty_question,
claim_not_in_registry (the @ns.name is not in the registry at the pinned
version), ambiguous_silence_reading (a boolean claim's question asks something
of the text and something about the world, so what a silent document means is
decided per call and the same document can answer False on one run and
Unknown on the next — the message names the words that caused it, and
declaring the reading clears it),
numeric_silence_instruction (a numeric question names a value to answer when
the document is silent, which contradicts the judge's own instruction that an
unstated number is Unknown), and model_not_served (the ruleset names a model
the LLM proxy does not serve, so runs will use the deployment default instead). A
blank question on a registry claim is not reported: that question is read
from the corpus when the run is configured. See
Readiness.
A version pin is part of a claim's identity
A run asks a registry claim once and records the one answer against every
rule that reads it — see One claim, one answer.
claim_version participates in that: two references to the same name are
the same claim only when their pins agree, so pinning one rule's reference
to a different version splits it off and it is asked separately. Unpinned
is its own identity, not a wildcard that matches any pin: every unpinned
reference to a name is the same claim as every other, and none of them is
the same claim as a pinned one, because an unpinned reference resolves
against the reading group's registry as it stands today and nothing says
that is the pinned version.
DSAIL itself has no syntax for a pin — @ns.name names a claim and says
nothing about its version — so a pin is set through the claims API and
travels with the claim: it is copied into each new ruleset version and into
a duplicated ruleset, and a DSAIL re-save never disturbs it.
Runs API
The platform exposes REST endpoints for creating and reading runs. These are fully documented in the Swagger UI and ReDoc references linked above. Two parts of POST /api/v1/runs/interactive are worth spelling out here, because both change what the request means rather than only what it contains.
A ruleset whose DSAIL does not compile is refused
Both POST /api/v1/runs/interactive and POST /api/v1/runs/batch build the run's configuration before starting anything, which compiles each rule's DSAIL. A rule that does not compile can never produce an assertion, so the request is refused with 400:
This ruleset version cannot be run: rule 'r35 broken' does not compile:
2 compilation errors: Line 2:0 - Syntax error: missing ';' at 'assert' ...
Every non-compiling rule in the version is named, with its own compiler message. The refusal covers the whole version rather than only the broken rule: a run that quietly evaluates the rules that do compile answers a narrower question than the one asked, and returns it in the shape of a complete answer.
GET /api/v1/rulesets/{id}/readiness reports the same condition as a compile_error warning with the line and column, before you submit. An interactive run is refused before any run row is written; a batch run's row exists by the time its configuration is built, so it is marked failed with the same text in error_message.
If a run reaches a workflow anyway and every rule in it failed to compile, the run ends failed rather than completed, with the compiler messages joined in error_message. A rule that fails at execution is deliberately not this case, even when it is the only rule: its step carries the diagnostic and the run completes so that diagnostic is what a reader sees, rather than a bare run failure.
supplied_answers — claim values you provide instead of extracting
An optional map of claim key to value. A supplied claim is not asked of the model; the rules are evaluated against the value you gave.
{
"ruleset_id": "<id>",
"ruleset_version": 3,
"supplied_answers": {
"@sys.freq_band_mentioned": { "value": "True", "source": "run:<earlier-run-id>" },
"@scg_alpha.item_waveform_x": "False",
"@sys.operating_frequency": "Unknown",
"has_disclaimer": true
}
}
- Keys are the claim keys of the ruleset version being run, exactly as its rules declare them:
@namespace.nameincluding the sigil for a registry claim, the bare identifier for a rule-local one. A registry claim supplied once binds in every rule that reads it. - Two value shapes coexist in one map: the bare value, and the object form
{"value": ..., "source": "..."}whosesourceis free text the platform stores and echoes without dereferencing. - Values are what extraction would have produced. Booleans take JSON
true/false, or one ofTrue/true/TRUE/Yes/yes/YESand their negative counterparts; numerics take a JSON number, or a string whose leading token is a number optionally followed by a unit ("12 GHz"); enums take a declared option, matched exactly first and then case-insensitively, and normalized to the declared spelling; strings take free text. The five spellingsUnknown,unknown,UNKNOWN,Unknown.andunknown.are legal for every type and mean leave the claim free, so the rule's completion policy governs it. - Two values extraction itself can produce are not accepted: the empty string, because it is indistinguishable from a caller who meant to send something, and the rate-limit sentinel, which is extraction's own infrastructure-failure marker.
- Omitting a claim is not an error. It stays Unknown, which is what makes a partial map well-defined.
- The map is stored on the run verbatim, in whichever shape you sent, so the run is its own audit of what was asserted rather than read. Normalization happens on the way to the solver, not on the way to storage.
input_documents is optional when nothing is left to extract. A claim's value can come from a document only if the claim is not entity-scoped, its rule compiles, and it has a non-blank question. input_documents may be omitted exactly when that set is a subset of the supplied keys; otherwise the request names what is still missing. With every such claim supplied, the run makes no model calls at all.
Every 400. Each names the claim it is about, because a value that would not bind is refused rather than dropped — a dropped value leaves the claim free and its rule decided by completion policy instead of by your evidence.
| Condition | Message |
|---|---|
| Key is not a claim of that ruleset version | supplied_answers['<key>'] is not a claim of ruleset '<id>' version <n> |
| Key is an entity-scoped claim | supplied_answers['<key>'] belongs to entity scope '<scope>' and is answered by instance extraction, so it cannot be supplied |
Object form with no value |
supplied_answers['<key>'] object form must contain a 'value' key (got keys: [...]) |
| Object form with an unexpected key | supplied_answers['<key>'] object form accepts only 'value' and 'source' (got also: [...]) |
Non-string source |
supplied_answers['<key>'].source must be a string |
null value |
supplied_answers['<key>'] value cannot be null; use "Unknown" to leave the claim unanswered |
| Empty value | supplied_answers['<key>'] value cannot be empty; use "Unknown" to leave the claim unanswered |
| List or object where a scalar belongs | supplied_answers['<key>'] value must be a single scalar (a string, number, or boolean), not a <type> |
| The extraction-failure sentinel | supplied_answers['<key>'] value '__RATE_LIMITED__' is reserved for extraction failures and cannot be supplied |
| Boolean claim, unreadable value | supplied_answers['<key>'] is a boolean claim; value <v> must be True or False (or "Unknown") |
| Numeric claim given a boolean | supplied_answers['<key>'] is a numeric claim; value <v> is a boolean |
| Numeric claim, unreadable value | supplied_answers['<key>'] is a numeric claim; value <v> must start with a number, optionally followed by a unit (e.g. 12 or "12 GHz"), or be "Unknown" |
| Enum value outside the domain | supplied_answers['<key>'] is an enum claim; value <v> is not one of its options [...] (or "Unknown") |
| Combined with streaming attribution | supplied_answers is not supported with streaming_attribution: the streaming path derives per-claim state from the document as it is chunked, so there is nothing for a supplied value to bypass |
| A gap left, and no document | input_documents is required unless every claim that would be extracted is supplied; still needing extraction: [...] |
A request with neither input_documents nor supplied_answers is still refused by the request validator with input_documents must contain at least one slot (422, unchanged).
supplied_answers comes back on the run's own representation: on the 202 from the POST and on GET /runs/{run_id}. The run list endpoint's summary shape does not carry it yet. An absent field and an empty map are kept apart end to end — null means no map was sent, {} means an empty one was.
Chunker tuning on streaming_attribution
Four optional fields tune how a streaming run divides its input; all are ignored
unless streaming_attribution is true. See
Streaming Attribution for what the modes mean.
| Field | Default | Meaning |
|---|---|---|
streaming_chunker_mode |
cumulative |
cumulative grows a prefix — chunk K is the document's first K steps. sliding walks a fixed window, so each chunk is an independent excerpt. |
streaming_chunker_window_size |
384 |
Chunk size in tokens: the cumulative step, or the sliding window's width. Capped at 8000, the prefix ceiling past which the cumulative chunker falls back to sliding windows. |
streaming_chunker_step_tokens |
follows window_size |
The cumulative step, named directly. When both are sent this one wins and the window size applies only to the fallback. |
streaming_chunker_overlap |
a quarter of the window | Sliding-window overlap. Cumulative chunks all start at the document's beginning, so it reaches them only through the fallback. Must be smaller than the window size, or the windower never advances. |
An incoherent combination is refused with 422 while you are still holding the request — an unknown mode, a window past the ceiling, an overlap at least as wide as its window. The chunker is built inside the run's own machinery, so without that check the same mistake would surface as a failed run instead.
slot_texts: what each slot actually held
Each result document carries slot_texts — {slot_name: text} — on any run
whose slot text arrived inline, which is every interactive run, single-slot and
multi-slot alike. It is the text each slot-bound claim was asked against, and it
is the only structural record of that: the run's input_text is a flattened
transcript that concatenates multi-slot inputs behind --- slot --- labels.
Reach for it whenever a claim's answer needs explaining, because
a claim sees one slot and an
Unknown produced by a fact living in the other slot is indistinguishable from
an extraction failure until you can see what its own slot held.
A batch run over a dataset carries slot_documents instead — {slot_name:
{document_id, document_name}}. There the activities fetch each slot's text from
the database rather than carrying it on the run, so the audit is reached through
the document id. slot_texts is null on that path, and the two fields are not
alternatives to check in order: which one is populated follows from how the run
was created.
Reading the attribution payload
A streaming run's results.attribution is a serialised provenance graph:
chunks, flips, state_vector, unresolved_keys. Two things about it are
easy to get wrong, and both only bite on a sliding run.
The offsets live in two different frames. A chunk's start / end are
offsets into the slot's source text — slot_texts[chunk.slot], above. A flip's
sentence_start / sentence_end are offsets into that chunk's text. To
place a flip's span in the document, add them:
doc_start = chunk.start + flip.sentence_start
doc_end = chunk.start + flip.sentence_end
On the default cumulative chunker every chunk starts at 0, so the two frames
coincide and code that conflates them reads correctly. Under
streaming_chunker_mode: "sliding" chunks start where the previous window left
off, and the same code silently reports text from the wrong part of the
document.
attribution_mode says how a flip was attributed, and it is per-flip rather
than per-run: "cumulative" when the flip's chunk was a growing prefix,
"windowed" when it was a sliding window. A sliding run reports "windowed"
throughout; a cumulative run over a document long enough to exceed the prefix
ceiling reports "cumulative" for its early chunks and "windowed" for the
fallback tail. Windowed chunks do not contain one another, so this is also the
signal that a claim could have been measured more than once — which is when a
boolean subtype decides the answer.
A flip's old_value is the value the state vector held when the flip's chunk
was measured, so replaying the flip log in order reproduces the run's history.
That holds for Bool.Scoped too, whose internal per-scope reset is not itself
an event in the log.
ablation_mode on POST /api/v1/runs/batch
document_free (the default) or corpus, for the ablation and
claim_quality batch modes; ignored by the others. See
Ablation Test.
Why a claim has no value
A claim result reads Unknown in three distinguishable situations, and the
fields keep them apart. Reading only actual_value cannot: a run whose every
model call was refused looks exactly like a run over a silent document.
| Field | Meaning |
|---|---|
rate_limited |
The provider throttled the question. actual_value is the display string Rate limited. Transient — re-running recovers it. |
extraction_failed |
The exception type that stopped the claim being asked, so the model was never successfully asked. Absent on a successful ask. Not a statement about the document. |
never_asked |
The claim carries no question text, so no call was made by design. This is the pre-run empty_question readiness warning, recorded on the run. |
None of the three set, with actual_value empty or Unknown, is the real
finding: the document was read and does not answer.
The run's CSV export gains mark_status, mark_level, mark_low, mark_high,
mark_unknown_driven and mark_unanswered_claims columns when any document in
the run carries a mark, so the qualifier travels with the level.
The mark
A run of a ruleset version that declares a Mark target carries the solved level alongside its per-rule results, as marked_<label> assert outputs plus structured fields: the level, its label and gloss, low and high, unknown_driven with the assumed_claims behind it, the bound and free claims, and the identity hash of the corpus slice the answer was solved over. unknown_driven is set when an unanswered claim decided the level — either because low and high differ, or because the rules themselves narrowed a claim nobody answered, which assumed_claims names; the interval alone does not carry it. A contradicted mark carries the minimal unsat core, the rules named and versioned, and a witness; an undetermined mark carries the reason no level may honestly be claimed. Nothing is added to the results of a ruleset with no Mark target. See The mark.
Rulesets API
POST /api/v1/rulesets/{ruleset_id}/publish freezes the current head version and opens a new working copy. For a ruleset version that declares a Mark target it is additionally gated on the corpus: the level a mark reports is a determination, and a determination is only as sound as the coherence of the rules behind it, so publishing one requires that coherence to have been proved.
The gate is satisfied when every corpus ruleset the version cites in its corpus_ruleset_refs is named by a publish-gate scope profile, every such profile stands on a verification report for its current slice, that report ran against today's coverage obligations, no error finding in it is left undispositioned, and between them those profiles' slices account for every corpus rule the mark would be solved from.
A ruleset with no Mark target is unaffected: the gate is not consulted, and the endpoint behaves exactly as it did before marking existed.
When the gate blocks, the response is 409 with a detail sentence naming what is wrong and what to do, plus the structured gate beside it:
{
"detail": "Cannot publish 'rs-scg': it declares a Mark target, so its publish is gated on clean corpus verification. No verification report for the current slice of profile 'Program RF'; re-verify before publishing.",
"ruleset_id": "rs-scg",
"version": 4,
"gate": {
"allowed": false,
"corpus_ruleset_refs": ["scg.alpha", "scg.cross"],
"profiles": ["sp.program_rf"],
"blockers": [
{"code": "no_report", "scope_profile_id": "sp.program_rf", "message": "...", "finding": null}
]
}
}
profiles lists the publish-gate profiles that answered for the cited rulesets. The blocker codes:
| Code | What it means | What to do |
|---|---|---|
no_corpus_rulesets |
The version declares a Mark target but names no corpus rulesets, so there is no slice to verify and the mark would be solved without the axioms that align the rules or the precedence that orders them. | Declare the corpus rulesets it answers to, or clear the Mark target. |
no_publish_gate_profile |
No publish-gate profile names one or more of the cited rulesets. The message names which, and what the profiles that do exist fall short of. | Declare a scope profile naming them, tick its publish gate, and re-verify. |
empty_slice |
A covering profile resolves to no rules, so it checks nothing and would pass anything. The message says which of the two causes it is. | Fix the profile's rulesets or scope tags, or clear its publish gate. |
no_report |
No verification report exists for that profile's slice as it stands now — either it was never run, or the corpus changed under it. | Re-verify in Corpus Health. |
stale_obligations |
The report predates a coverage obligation declared against its scope, so it never checked it. Declaring an obligation leaves the slice hash unchanged, which is why this is a separate condition. | Re-verify. |
undisposed_error |
An error finding in a covering profile's report. | Resolve it in the corpus, or record a disposition for it, then re-verify. |
unverified_rules |
Rules the mark would be solved from that no publish-gate profile checked — a profile pinned to one corpus ruleset version while the ruleset cites every version, or scope tags that drop a rule a run keeps (a run resolves its marking slice unscoped). The rules are named. | Widen a publish-gate profile to cover them and re-verify. |
Two conditions read differently here than they do for the corpus's own publish gate, and both differences follow from the subject being a ruleset:
- An empty gate blocks rather than passes. A corpus rule with no publish-gate profile declared has no gate; a marking ruleset names the corpus rulesets it answers to, and the mark's authority is the verification of exactly those, so nothing having checked them is the condition to report.
- Warnings never block, and a dispositioned error does not. A ruleset appears in no finding's core, so the rule gate's "warnings implicating this version" clause has no analogue. Errors block until the finding is dispositioned, which is how a team accepts one on the record.
Authentication
All API endpoints require authentication. The API accepts two types of Bearer tokens:
Option 1: Authentik API Token (simplest)
Use the Authentik API token directly as a Bearer token. The platform validates it against Authentik and resolves the bound user's identity and group memberships automatically.
-
Obtain an API token from a Jaxon platform administrator. See Creating a Service Account and API Token for step-by-step instructions.
-
Include both headers on every API request:
curl -k https://<host>/api/v1/projects \ -H "Authorization: Bearer <api-token>" \ -H "X-Jaxon-Active-Group: <group-name>"For development environments with the default service account:
curl -k https://<host>/api/v1/projects \ -H "Authorization: Bearer jaxon-dev-api-key" \ -H "X-Jaxon-Active-Group: Jaxon Users"
API tokens do not expire unless configured to do so in Authentik.
Option 2: OAuth 2.0 JWT Access Token
For integrations that already have an OAuth 2.0 flow (e.g., the Jaxon UI uses PKCE), JWT access tokens obtained from the Authentik OIDC provider are also accepted:
curl -k https://<host>/api/v1/projects \
-H "Authorization: Bearer <jwt-access-token>" \
-H "X-Jaxon-Active-Group: <group-name>"
JWT access tokens expire after 1 hour.
Required Headers
| Header | Required | Description |
|---|---|---|
Authorization |
Yes | Bearer token (Authentik API token or JWT access token) |
X-Jaxon-Active-Group |
Yes | The Authentik group to use as the active context for the request. The authenticated user must be a member of this group. |