Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.aethis.ai/llms.txt

Use this file to discover all available pages before exploring further.

The public API returns a detail object on every 4xx / 5xx, typically shaped:
{
  "detail": {
    "error": "<category>",
    "reason_code": "<stable identifier>",
    "message": "<human-readable explanation>"
  }
}
A handful of endpoints return detail as a plain string (notably 404s from lookup endpoints). In those cases there’s no reason_code; match on the status code and message. For a working debugging flow using these codes, see Debug a failing decide.

By reason_code

missing_api_key — 401

{
  "detail": {
    "error": "unauthorized",
    "reason_code": "missing_api_key",
    "message": "X-API-Key header is required for this endpoint."
  }
}
The endpoint requires an API key and you didn’t send one. Anonymous /decide accepts ruleset_id on public rulesets; rulebook_id and all authoring endpoints do not. Add an x-api-key header.

invalid_api_key — 401

{
  "detail": {
    "error": "unauthorized",
    "reason_code": "invalid_api_key",
    "message": "API key is invalid or has been revoked."
  }
}
The key doesn’t match any active record — typo, wrong environment, or revoked. Re-run aethis login to mint a fresh one, or retrieve the correct key from your credential store.

api_key_expired — 401

{
  "detail": {
    "error": "unauthorized",
    "reason_code": "api_key_expired",
    "message": "API key has expired. Create a new one via 'aethis account generate'."
  }
}
Create a new key. Expiring keys are uncommon on the public platform today but will become standard when scheduled rotation lands.

denied_missing_permission — 403

{
  "detail": {
    "error": "forbidden",
    "reason_code": "denied_missing_permission",
    "action": "scope.rulebooks:write",
    "missing_permissions": ["rulebooks:write"],
    "message": "API key missing required scope: 'rulebooks:write'"
  }
}
Your key authenticated but lacks the scope the endpoint requires. missing_permissions is the authoritative list. For most authoring flows you’ll want decide, rulesets:read, rulesets:write, projects:write; composed rulebooks additionally need rulebooks:read / rulebooks:write. See REST API scopes.

reserved_namespace — 403

{
  "detail": {
    "error": "forbidden",
    "reason_code": "reserved_namespace",
    "message": "Slug namespace 'aethis/' is reserved for internal use."
  }
}
You tried to publish a ruleset or rulebook under a reserved slug prefix (aethis/*) without an internal-tier key. Either pick a different namespace (my-team/*, my-company/*) or request an internal key via support.

invalid_slug_format — 422

{
  "detail": {
    "error": "validation_error",
    "reason_code": "invalid_slug_format",
    "message": "Slug must match ^[a-z][a-z0-9-]*(/[a-z][a-z0-9-]*)*$"
  }
}
Slugs are lowercase ASCII letters + digits + hyphens, organised into /-separated segments, starting with a letter. Common causes: uppercase letters, spaces, trailing /, leading -. See Nomenclature.

slug_conflict — 409

{
  "detail": {
    "error": "conflict",
    "reason_code": "slug_conflict",
    "message": "Slug 'my-team/my-ruleset' is already in use by another tenant."
  }
}
Slugs are globally unique across tenants. If you own the slug on a prior ruleset (same tenant), the publish flow will transfer it to the new ruleset automatically. If a different tenant holds it, you need a different slug.

daily_quota_exceeded — 429

{
  "detail": {
    "error": "rate_limit_exceeded",
    "reason_code": "daily_quota_exceeded",
    "message": "Daily quota exceeded for 'decide' (limit: 250/day). Upgrade your tier or wait for UTC midnight reset."
  }
}
Response includes a Retry-After: 86400 header. Upgrade your tier via support, wait for UTC midnight reset, or authenticate with a higher-tier key.

rate_limit_backend_unavailable — 503

{
  "detail": {
    "error": "service_unavailable",
    "reason_code": "rate_limit_backend_unavailable",
    "message": "Rate limit service is temporarily unavailable. Please retry."
  }
}
Transient. Retry with exponential backoff. If it persists, check status or ping support with the decision_id from the most recent successful call for correlation.

Bare-string 404s (no reason_code)

Lookup endpoints return detail as a string rather than an object when the target doesn’t exist:
{ "detail": "Ruleset 'aethis/uk-fsm' not found" }
Common causes:
  • Slug typo. Whitespace, capitalisation, or a / mismatch.
  • Using ruleset_id to look up a rulebook slug. aethis/uk-fsm is a rulebook slug; it won’t resolve from the ruleset_id field. Move the value to rulebook_id and add x-api-key. See Nomenclature.
  • Ruleset is private. Anonymous /decide only returns public rulesets; private rulesets 404 from an unauthenticated caller to avoid leaking their existence.
  • Slug not yet resolved on this endpoint. /explain-failure does not currently resolve slugs (only concrete ruleset_ids) — track aethis-core#51. Use the ruleset_id returned in your /decide envelope instead.

Validation errors from FastAPI (422)

If you send a malformed request body — missing required field, wrong type, unknown enum value — FastAPI returns its own 422 shape:
{
  "detail": [
    {
      "type": "missing",
      "loc": ["body", "expected_outcome"],
      "msg": "Field required",
      "input": { "field_values": { ... } }
    }
  ]
}
loc pinpoints the offending field. Common causes in authoring flows:
  • Omitting expected_outcome on POST /rulesets/{id}/explain-failure
  • Sending outcome_logic as { "expr": "A AND B" } on POST /rulebooks/ — the server requires a full Expr AST since aethis-core 0.7.2. See Author a rule.

Source of truth

This page is hand-maintained. The authoritative list of reason_code values lives in the server source under aethis-core/aethis_core/public/ — grep for reason_code to enumerate. When a new reason_code is introduced, update this page in the same PR.