Skip to content

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.json shipped 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 project
    • include_deleted (optional, boolean, default false) — include soft-deleted Documents
    • types (optional, string) — comma-separated list of Document types to include. Allowed values: gold, silver, bronze, or the sentinel all.
      • When omitted or empty, the endpoint returns gold and silver Documents only (bronze excluded).
      • types=bronze returns only bronze Documents.
      • types=gold,silver returns gold and silver (equivalent to the default).
      • types=all returns every Document type including bronze.

    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=all
    

    Breaking change (Phase 38.3)

    Prior to this release, GET /documents with no types parameter 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 pass types=all explicitly.

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.name including 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": "..."} whose source is free text the platform stores and echoes without dereferencing.
  • Values are what extraction would have produced. Booleans take JSON true / false, or one of True / true / TRUE / Yes / yes / YES and 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 spellings Unknown, unknown, UNKNOWN, Unknown. and unknown. 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.

  1. Obtain an API token from a Jaxon platform administrator. See Creating a Service Account and API Token for step-by-step instructions.

  2. 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.