Skip to main content
Use the REST API when you’re building your own backend, dashboard, or custom workflow — a lending application that calls /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.
Examples below marked (illustrative) use placeholder digests and identifiers; the field names and shapes are exact.

Authentication

Get an API key: sign up Omitting the header on decision endpoints works fine. Including it on decision endpoints is also fine — it’s ignored.

Scopes

API keys carry a set of scopes. Each authoring endpoint requires a specific scope; a key without that scope receives 403 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.
Rulebook lookups and decisions require an API key. Anonymous /decide resolves a ruleset_id or ruleset slug against public rulesets only. The separate rulebook_id field (for composed multi-section rulebooks like aethis/uk-fsm) is always scope-gated — anonymous callers get a 401. Anonymous access to rulebooks is limited to the GET /rulebooks/ catalogue listing; to evaluate one, hit each section by slug instead, or pass an x-api-key header with a key that has the decide scope. See Nomenclature for the full distinction.

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.
Two consequences worth designing around.
  1. GET /rulebooks/ accepts anonymous requests server-side, but it is not on the open browser surface — call it from your backend.
  2. Never ship an authoring key to a browser. Even setting CORS aside, an x-api-key in client-side code is a published credential.
Client-side use is intended for the evaluation surface: decide against a public ruleset, read its schema, explain it, graph it. Anything that writes belongs on your server.

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.
Response (illustrative — digests and IDs are placeholders; field names and shapes are exact):
Parameters:
  • ruleset_id — the published ruleset to evaluate against
  • field_values — map of field names to values (types must match the ruleset schema)
  • include_trace (optional) — when true, returns a trace object showing how each criterion was evaluated and the source clause it references
  • include_explanation (optional) — when true, returns a structured explanation object: 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.
Decisions:
  • eligible — all criteria satisfied
  • not_eligible — one or more criteria failed
  • undetermined — 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.
Everything else is advisory and never gates the outcome:
  • missing_fields, next_question, optimal_path — progress guidance while the decision is undetermined;
  • 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.
Per-criterion status is not a second decision. When field_errors forces undetermined, explanation.groups[].status and graph_overlay.nodes[].overlay.status still report what the engine established for each criterion from the valid subset of your inputs — so a criterion can legitimately read "satisfied" in the same response.That is deliberate: those fields answer “what is true of this criterion?”, while decision answers “what may I act on?”. Never recompute an outcome by aggregating group or node statuses. decision is the only field to treat as the result, and you read field_errors first.

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[].
What the contract guarantees, and what it does not:
  • quote.exact is 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 /explain does not fetch anything while you wait.
  • content_digest fixes the bytes the quote was verified against, and verified_at says when. If the authority silently rewrites the page, your citation still names what was actually cited.
  • deep_link locates the quote in the source: a percent-encoded #:~:text= fragment for HTML and text, #page=N for PDF.
  • licence is mandatory. Reproducing text under no stated licence is not publishable.
  • schema_version grows additively. Pin schema_version >= 1 and 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 to content_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:
On a v2 reference, url is the relative path of an authenticated download route — /api/v1/public/projects/{project_id}/sources/{source_id}/raw, requiring a key with the projects:read scope. Resolve it against the engine base URL you are calling, and never render it as a public link. v1 url is an absolute, publicly fetchable HTTPS URL; the two are not interchangeable.
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:
Read it on every page. 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 an X-Anthropic-Key header on generation requests — used for that request only, never stored.

Step 1 — Create a project

Step 2 — Generate and test

If tests are failing:
Add guidance and regenerate:
Then repeat generate-and-test.

Incremental refine (minimal edit)

Both generate 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

Now evaluate with /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 with projects: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.
Response fields:
  • score (int | null) — the weighted rubric score (0–100). null on 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), the evidence that produced it, its weight, whether it’s scored, the why it matters, the actionable_via lever that changes it, and a docs_url linking 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 when coach: true.
The full rubric — every check, its weight, and its pass/warn/fail thresholds — is documented on the Review checks page.
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:
422 — a top-level request key the API does not define:
The request body is strict. An undefined key is rejected rather than ignored, so a client sending an unimplemented field finds out immediately instead of believing a feature exists.
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.
429 with rate limit headers:
The forward-visibility headers ride ordinary metered responses too — check 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.