/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.
api.aethis.ai serves engine 0.49.1, which is the contract these pages
describe; every response carries engine_version so you can confirm which
build answered you.
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 and
include_graph_overlay adds 5 further units, so a call with trace and
explanation costs 11. Budget accordingly, or use a key.
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.
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;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.
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. Pass anX-Anthropic-Key header on generation requests — used for that request only, never stored.
Step 1 — Create a project
Step 2 — Generate and test
generate-and-test.
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.
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.
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.