/decide on every form submission, a compliance portal that displays current rule explanations, or a custom authoring pipeline.
Base URL: https://api.aethis.ai/api/v1/public/
Evaluation and read endpoints on public rulesets accept anonymous requests — no key.Authoring endpoints require an
x-api-key header, are invite-only, and must be called from your server.
Only an enumerated set of routes is reachable from a browser on an arbitrary
origin — see Browser and mobile access
before you call this API from client-side code.
Successful decision responses identify the serving build in
engine_version.
Read that field from your response rather than relying on a version copied into a tutorial.
What the deployed engine serves.Authentication
Scopes
API keys carry a set of scopes. Each authoring endpoint requires a specific scope; a key without that scope receives403 Forbidden. For the CLI-side view — what aethis login mints, how to check with aethis whoami, and how to mint a rulebook-capable key — see Authentication & API keys.
Decision endpoints (
/decide, /schema, /explain on public rulesets) require no scope and accept anonymous requests. GET /rulebooks/ also accepts anonymous requests: without a key it returns the cross-tenant public catalogue (rulebooks with visibility=public and status=active); with a key it returns your tenant’s rulebooks as before.
Browser and mobile access (CORS)
Cross-origin access is scoped per route and method, not granted to “decision endpoints” as a class. Two regimes: Open surface — any origin, no credentials. Exactly these (method, path) pairs answer a cross-origin request from any website:
On this surface the engine allows
GET, POST and OPTIONS, accepts the
Content-Type and X-API-Key request headers, and never returns
Access-Control-Allow-Credentials — cookies and other ambient credentials are
not carried, by design.
Restricted surface — everything else. Every other route, including all
authoring and project routes, grants CORS only to first-party Aethis origins.
A browser request from your own origin to an authoring route receives no CORS
grant and the browser blocks it. Preflights are classified by the method the
browser asks for, so an OPTIONS preflight for a non-open method on an open
path is handled by the restricted policy.
Rate limits
Every authenticated request is metered against one of six operation classes. A class is both the rate-limit bucket and the usage metric, so what you’re throttled on is exactly what you can measure:
Limits apply per rolling 24-hour window (hourly buckets that slide continuously, not a calendar-day reset). Budgets are generous everywhere except
generate, which carries the only meaningful ceiling — so ordinary authoring and decisions no longer compete with generation for a shared quota:
generate limits currently run in report-only mode: an over-limit request is recorded but not rejected while the ceilings are tuned against real usage. Every other class is enforced. Tier is set at key creation — contact eng@aethis.ai to upgrade.Forward-visibility headers
Every metered response carries your current budget, so a 429 is never the first signal:X-RateLimit-Class— the operation class this request was metered against.X-RateLimit-Limit— the limit for that class on your tier.X-RateLimit-Remaining— requests left in the current rolling window.X-RateLimit-Reset— epoch seconds of the next hourly boundary, when the oldest counted hour ages out and budget is freed.
Check your usage
GET /usage returns your budget and usage across every operation class. It is scoped to the calling key and is never metered — poll it as often as you like without consuming quota:
classes[] gives each class’s rolling-24h budget and usage; rolling adds 7-day and 30-day per-class totals. See the API reference for the full schema.
Anonymous budgets
Anonymous callers are metered separately, and on more than one axis. Plan for all four when you build on the open surface:
Expensive options are costed. A plain anonymous
/decide charges one unit.
Each of no_cache, include_trace, include_explanation,
include_graph_overlay and include_choice_context adds 5 further units,
so a call with trace and explanation costs 11. Budget accordingly, or use a
key. include_choice_context is useful only with an authenticated rulebook
decision; it is a no-op on a leaf ruleset even though the anonymous cost still
applies when the flag is sent.
Client identity comes from the trusted proxy hop, not from a caller-supplied
X-Forwarded-For. Rotating that header does not rotate your anonymous quota.
Anonymous request bodies are capped at 2 MB on the evaluation routes and
rejected with 413 before the body is parsed.
What an anonymous call stores. Anonymous preview decisions persist the
input hash and aggregate metadata only — never your raw
field_values
and never a caller reference. inputs_hash is deliberately unsalted, so you
can recompute it yourself from the same inputs and match a stored decision
for replay.Decision envelope
Every/decide response includes audit fields (decision_id, inputs_hash, engine_version) for reproducible replay without the server echoing your inputs. See Decision envelope → for the full contract.
Resolved identity
For a published leaf ruleset the response always resolves what you asked for into immutable identity, whether you passed a slug or an ID and whether the answer came from cache or cold:
Pin replay and audit to
content_digest, not to the version label. A
republish of byte-identical content can advance the label while reusing the
existing content cut; only the digest identifies the rules that produced a
given decision.
ruleset_version: "unknown" is not a possible value for a published leaf
ruleset. Composed rulebook (rulebook_id) responses still report "unknown"
for the composition until resolved member identity lands.
Evaluate eligibility
No API key required.ruleset_id— the published ruleset to evaluate againstfield_values— map of field names to values (types must match the ruleset schema)include_trace(optional) — whentrue, returns atraceobject showing how each criterion was evaluated and the source clause it referencesinclude_explanation(optional) — whentrue, returns a structuredexplanationobject: gate-level checklist (groups[].criteria[].status), the supporting facts that proved each satisfied criterion (supporting_facts), the satisfied requirement name (decision_path), and any provided fields the ruleset never references (unused_facts— useful for catching field-name typos). See Debug a /decide for the full payload shape.include_choice_context(optional) — whentrueon arulebook_iddecision, returns compact, answer-aware context for conversational route choices. It isfalseby default and a no-op on a leafruleset_iddecision.inquiry_context(optional) — supplies held question instructions and navigation preferences for this decision. It does not add facts tofield_valuesor change the eligibility verdict.
Conversational choice context
Setinclude_choice_context: true when a conversational interface needs to
offer authored alternatives without downloading or interpreting the full
dependency graph:
choice_context is populated only for that opt-in rulebook
call:
field_sections is the complete field-to-section membership map.
choice_points contains authored OR-groups; each alternative is open,
closed, or satisfied for the supplied answers. offerable is true only
when no alternative is satisfied and at least two remain open. Use
include_graph_overlay instead when you need the full graph for visualisation
or debugging. Without the opt-in flag, and on leaf ruleset decisions,
choice_context is null.
Inquiry navigation
Useinquiry_context when an intake flow needs to keep a question open without
inventing an answer, or wants Core to apply a one-turn navigation preference.
The context is request-local. It contains only exact field, section, and route
IDs from the selected schema; it is not a substitute for field_values.
The JSON fragments in this section show the contract shape. Use IDs returned by
the schema for a live request.
held[].reason is either unknown or postponed. navigation.intent is
continue, leave, or go_to; only go_to carries section_id.
selected_route is optional. The API validates every supplied ID against the
selected schema and rejects malformed, conflicting, duplicate, or
non-applicant instructions.
This public Free School Meals ruleset has two applicant fields. The following
REST call holds both without asserting either as a fact:
inquiry_selection:
reason is engine_order, explicit_section, route_preference,
section_continuity, or null. The satisfaction fields are true, false,
or null when the corresponding preference was not supplied. They explain the
question selection only; they do not change decision.
When every currently askable applicant field is held, the response can include
an inquiry_outcome instead of a next_question:
awaiting_revisit means held instructions mask the immediate question; it is
not an eligibility outcome and does not add an undetermined_reason value.
The existing more_to_ask reason remains correct because unheld selection
would otherwise continue. Existing terminal, non-applicant, input-error, and
review boundaries take precedence and omit inquiry_outcome.
Omit inquiry_context to retain the existing request and response shape. The
official Python SDK, CLI, and MCP server do not yet expose this opt-in; use the
REST API until their linked releases ship.
Decisions:
eligible— all criteria satisfiednot_eligible— one or more criteria failedundetermined— the engine could not reach a decision (missing field, discretionary clause, blocking input error, or a case outside the compiled rules)
Reading the response
Blocking errors versus advisory signals
field_errors is the only blocking channel in a /decide response. Every
entry means an input you sent could not be applied to the ruleset: an unknown
field key, a value that does not convert to the field’s type, or a
conversion failure at evaluation time.
A non-empty field_errors always forces decision: "undetermined". The
engine never returns eligible or not_eligible next to a blocking input
error, because that verdict would have been computed from a partial input set
you did not knowingly send. Read field_errors before you treat any
decision as terminal.
missing_fields,next_question,optimal_path— progress guidance while the decision isundetermined. Withinquiry_context, a nullnext_questionandundetermined_reason: "more_to_ask"can instead meaninquiry_outcome.status: "awaiting_revisit"; read that opt-in member before treating the question flow as complete;explanation.unused_facts— answers you sent that no satisfied criterion referenced (usually a field-name typo);- a malformed
caller_ref— dropped with a server-side warning, never a rejection.
The response truth table
A
500 returns an opaque envelope: no partial decision, and none of your
submitted values echoed back.
Unknown top-level request keys are rejected, not ignored: the request body
is strict, and an undefined key returns 422 with
detail[].type == "extra_forbidden". If you have code that sends a field this
API does not document, it has never been doing what you thought.
Trace, explanation, and source references
Three distinct things, often conflated. Ask for the one you need — on the anonymous surface each costs extra quota.trace is not a citation. It shows the compiled logic that ran. The
citation lives in source_references, which is validated at publish time
rather than assembled at read time.
The SourceReference contract
GET /rulesets/{id}/explain and POST /decide with include_explanation: true
return the identical SourceReference shape — on /explain it sits at
criteria[].source_references[], on /decide at
explanation.groups[].criteria[].source_references[].
quote.exactis verbatim, never a summary. Publish validation asserts the quote occurs in the fetched source after whitespace normalisation (HTML is tag-stripped, PDF is checked against extracted page text). No stemming, no case folding, no fuzzy matching.- References are resolved and verified at publish time, not at read time.
An unresolvable, private, unlicensed or digest-mismatched reference fails
the publish. You are reading a check that already passed, so
/explaindoes not fetch anything while you wait. content_digestfixes the bytes the quote was verified against, andverified_atsays when. If the authority silently rewrites the page, your citation still names what was actually cited.deep_linklocates the quote in the source: a percent-encoded#:~:text=fragment for HTML and text,#page=Nfor PDF.licenceis mandatory. Reproducing text under no stated licence is not publishable.schema_versiongrows additively. Pinschema_version >= 1and treat new fields as optional.- Newly emitted v1 references carry an optional
snapshot. It is the digest keying the engine-retained copy of the bytes it fetched (equal tocontent_digest), so the citation outlives the page it came from. Absent on references stored before snapshot-on-fetch.
Schema v2: artefact-backed references
A reference with"schema_version": 2 cites a file the author uploaded to
their project rather than a public URL. It adds target_kind: "artefact",
artefact_project_id and artefact_source_id, and its url is different in
kind from a v1 url:
Artefact references are private-only. Any operation that would make an
artefact-backed ruleset publicly resolvable — publishing it public, flipping
ruleset or rulebook visibility, attaching or promoting it into a public rulebook
— is rejected with artefact_reference_public_visibility_forbidden. So a
publicly readable ruleset’s references are always v1 URL citations, and no
anonymous response ever exposes a private project or source identifier.
Full authoring semantics, including the failure reason codes, are on
Provenance and citations.
source_references is present only on rulesets published under this
contract. Older published rulesets may carry the legacy source_refs array
of opaque authoring keys instead. Read source_references when it is
present and fall back rather than assuming either.Inspect a ruleset
notes (each FieldNoteOut: { note_text, source, metadata }) is the
structured guidance the ruleset author attached during /fields/discover.
Conversational front-ends (e.g. a paralegal bot) typically render
metadata.type='why' notes when the user asks “why are you asking this?”
and draw on metadata.type='legal_background' notes for edge-case
follow-ups. Older bundles return "notes": [] — treat the array as
optional.
weight is the question-ordering hint used by the constraint solver (higher = less preferred to
ask). Used by /decide’s optimal_path to surface cheap questions before
expensive ones when multiple ways to satisfy the ruleset exist.
enum_labels is an optional {member slug: display label} map the ruleset
author attached to an Enum field, for front-ends that store the slug but must
show a human-readable label. canonical_field is the author’s pairing
between this eligibility field and the consuming system’s own storage key for
the same fact. Both are carried by the engine and never interpreted by it —
nothing validates enum_labels against enum_values. Both are always present
on the response and are null when the author declared neither; read them with
an explicit null check, since {} means “no labels” and is a different
statement from null.
Honest catalogue counts
GET /rulesets returns a bare JSON array. A stored record that fails
validation on read is skipped rather than 500-ing the whole catalogue, which
means a page can be shorter than the records the query matched. Every
response carries a header saying how many were dropped:
0 is the normal case; a non-zero value tells you a
short page is short because something was skipped, not because you reached the
end of the catalogue. Never infer exhaustion from len(page) < limit.
A direct read of a corrupt record is never silently skipped: it returns a
structured 422 with reason_code: "ruleset_record_corrupt", so the failure
is attributable to one record rather than surfacing as a page-wide 500.
Same anonymous-public access applies to GET /rulesets/{id}/graph — the compiled ruleset as a node/edge graph plus a ready-to-render Mermaid string, for visualizing structure instead of reading a flat field list. See Ruleset & rulebook graphs.
Author rules
Authoring is invite-only private beta (request access). API key required; do all authoring server-side. The literal flow is create project → upload source → discover and review fields → store reviewed tests → start one generation job → poll that job → test → publish → decide. See Author your first ruleset for the complete executable sequence. The provider credential belongs inX-Anthropic-Key on discovery and default Sonnet generation requests. The DeepSeek generation option uses X-DeepSeek-Key, as described below. Read it from your server’s environment; never send it in JSON, a command argument, or a client-side application. The API uses it only for the request and does not store it.
Structured acceptance contracts
Store the full versioned contract atomically withPOST /projects/{project_id}/tests. The raw REST envelope requires
replace: true:
aethis generate --acceptance-contract contains
contract_version, test_cases, and optional expected_review_bindings, but
does not contain replace; the CLI supplies the REST replacement envelope.
See structured acceptance contracts
for assertion semantics and clearing rules.
Generation model selection
BothPOST /projects/{project_id}/generate and
POST /projects/{project_id}/generate-and-test accept an optional model
field alongside mode and seed_ruleset_id:
Omit
model (or send null) to preserve the server’s configured default:
Sonnet 5 unless the server operator has overridden it. Other model selectors
are rejected. The model applies to this request only.
For example, the body for DeepSeek refinement is:
X-API-Key for Aethis authentication and only the selected provider’s
credential header. Never put provider keys in the JSON body. External callers
must bring that provider’s key: the API does not substitute a platform
credential or another model when it is missing. This option covers generation
and refinement; discovery continues to use X-Anthropic-Key.
Recover an asynchronous generation
For a section that may exceed the synchronous endpoint’s request timeout, usePOST /projects/{project_id}/generate, then poll
GET /projects/{project_id}/status. Send your provider credential in the
selected provider’s header on the generation request only; it is never stored.
If a client timeout interrupts polling, check status before attempting another
generation. A current response advertises generation_contract_version: 1 and
provides top-level telemetry_availability, server-authoritative
worker_lifecycle, and retry_readiness. Retry only when readiness is ready;
blocked means a job still owns the project and cleanup_pending means server
cleanup has not finished. Its job object provides progress and timestamps,
safe failure diagnostics, and live convergence fields such as
current_turn, best_passed, test_total, last_tool, and
seconds_since_progress.
POST to
/projects/{project_id}/generate/cancel?job_id={observed_job_id}. Copy the id
from a successful status response; binding cancellation to that exact job means
a delayed request cannot cancel a newer run on the project. This marks the job failed and
releases its project ownership; it does not guarantee an already-running worker
stops immediately. Inspect outcome (cancelled or idempotent
already_cancelled), detail, and project_released
before starting another run. Never issue cancellation automatically because a
client timed out or progress looks stalled.
Incremental refine (minimal edit)
Bothgenerate and generate-and-test accept an optional mode body. mode: "refine" seeds generation from the section’s active ruleset and makes the minimal edit to fix failing tests, instead of re-authoring the section from scratch. Omitting the body (or mode: "fresh") is the default from-scratch behaviour. Supply the selected provider key in its header (X-Anthropic-Key for Sonnet), never in a JSON field. The optional model selection above also applies to refinement.
seed_ruleset_id may be supplied to refine from a specific ruleset; when omitted, the section’s active ruleset is used.
Step 3 — Publish
/decide using the returned ruleset_id.
Promote a testing ruleset to live
When you publish into a rulebook, the candidate remains intesting until an
explicit, atomic promotion. Call
POST /rulebooks/{rulebook_id}/rulesets/{ruleset_name}/promote-to-live with a
rulebooks:write key. For a two-segment slug, use
/rulebooks/{namespace}/{name}/rulesets/{ruleset_name}/promote-to-live.
force_unsafe defaults to false. A non-conservative presence operator — one
whose unanswered default can be more favourable than answering the field — is
refused with 422 non_conservative_presence_op. Only an internal Aethis key may
set force_unsafe: true; the bypass is audit-logged, and an external key is
still refused. Correct the rule instead of designing an integration around the
internal override.
presence_polarity_advisories is null after a clean promotion. It contains
field, criterion, and message entries only when the promotion succeeded
despite a polarity finding: the audited internal force_unsafe path, or the
advisory path for an already-published first-party showcase member.
A non-null list is evidence that promotion completed with a known semantic
risk, not evidence that the gate blocked it.
Review a project (Authoring Coach)
Run the versioned authoring rubric over a project and get an objective quality report — a reproducible score, per-check evidence across grounding / process / lifecycle, strengths, and the single highest-leverage next improvement. Requires an API key withprojects:read + rulesets:read. Advisory only — it never blocks generation, publishing, or a decision.
The deterministic report needs no LLM key. Add coach: true (with your own X-Anthropic-Key) to get an LLM-synthesised coaching narrative on top; the deterministic layer ignores the key entirely.
score(int | null) — the weighted rubric score (0–100).nullon a project too thin to score.data_completeness—"ok", or"thin"when the project is missing a ruleset, tests, or sources (the report degrades gracefully, never a 500).checks[]— one entry per rubric check:status(pass/warn/fail/na/info), theevidencethat produced it, itsweight, whether it’sscored, thewhyit matters, theactionable_vialever that changes it, and adocs_urllinking to Review checks.strengths[]— what the project already does well.next_skill(object | null) — the single highest-leverage, author-actionable improvement to make next.coaching(string | null) — the LLM narrative, present only whencoach: true.
Ambient review hints. The
generate, generate-and-test, and publish responses now carry an optional review_hint — the top author-actionable warning from the same rubric, so you get a nudge in-flow without an explicit review call. It’s the same shape as next_skill (check_id, message, actionable_via, docs_url) and is null when nothing needs attention.Error responses
422 — a value that failed validation:
A bad field value does not produce a
422 on /decide — it comes back as
a 200 with the offending key in field_errors and decision: "undetermined". 422 is reserved for a malformed request. See
the truth table.X-RateLimit-Remaining or poll /usage to stay ahead of a 429.
Date fields take an ISO "YYYY-MM-DD" string or an integer ordinal on ruleset evaluations (ISO accepted since engine 0.31.0). Rulebook evaluations (rulebook_id) still require ordinals. See Date field values.Full endpoint reference
All endpoints with schemas, parameter details, and example responses: API Reference →Help improve this pageIf something here is unclear or missing an example, use the feedback button at the bottom of the page.Found a bug? Open a GitHub issue. Evaluating Aethis for a regulated workflow? Contact us directly.