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

# Errors reference

> Every reason_code the Aethis public API emits, plus recovery actions.

<Note>
  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](/reference/deployed-contract).
</Note>

The public API returns a `detail` object on every 4xx / 5xx, typically shaped:

```json theme={null}
{
  "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](/recipes/debug-a-decide).

<Note>
  **A 401 or 403 is usually a tier boundary, not a bug.** Evaluating a
  published public ruleset needs no key at all — if you are getting `401`,
  check you are sending `ruleset_id` with a leaf slug rather than
  `rulebook_id`, which is always key-gated. Authoring and publishing are
  invite-only: [request access](https://aethis.ai/developer-access). Which
  operations sit in which tier: [Capabilities and access](/reference/capabilities).
</Note>

<Warning>
  **A `200` is not automatically a success on `/decide`.** A non-empty
  `field_errors` object means an input could not be applied and forces
  `decision: "undetermined"` — a positive verdict never appears beside one.
  Read `field_errors` before you read `decision`. See
  [the response truth table](/interfaces/rest-api#the-response-truth-table).
</Warning>

***

## By reason\_code

### `missing_api_key` — 401

```json theme={null}
{
  "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

```json theme={null}
{
  "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

```json theme={null}
{
  "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

```json theme={null}
{
  "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 are gated on `rulebooks:read` / `rulebooks:write`, which a key holding the corresponding `rulesets:*` scope currently satisfies through a transition alias — so a login key works today, but name the rulebook scopes explicitly on any key you intend to keep. Run `aethis whoami` to see the scopes your key actually carries. See [Authentication & API keys](/reference/authentication) for the full scope model, or [REST API scopes](/interfaces/rest-api#scopes).

<Warning>
  Keys minted by `aethis login` (and `aethis account generate` with no `--scope`) do **not** include `rulebooks:*`, so your first `aethis rulebooks create`/`publish` will 403. Mint a rulebook-capable key by passing the full scope set explicitly — `--scope`/`-s` replaces the default list, so include every scope you want:

  ```bash theme={null}
  aethis account generate --name rulebook-authoring \
    -s decide -s projects:read -s projects:write \
    -s rulesets:read -s rulesets:explain -s rulesets:write \
    -s rulebooks:read -s rulebooks:write
  ```
</Warning>

### `reserved_namespace` — 403

```json theme={null}
{
  "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/*`). Pick a different namespace (e.g. `my-team/*`, `my-company/*`).

### `invalid_slug_format` — 422

```json theme={null}
{
  "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](/concepts/nomenclature#the-two-objects).

### `slug_conflict` — 409

```json theme={null}
{
  "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

```json theme={null}
{
  "detail": {
    "error": "rate_limit_exceeded",
    "reason_code": "daily_quota_exceeded",
    "message": "Quota exceeded for 'generate' (limit: 200 per rolling 24h). Upgrade your tier or wait for the window to slide.",
    "category": "generate",
    "tier": "free",
    "limit": 200
  }
}
```

`category` is the [operation class](/interfaces/rest-api#rate-limits) the request was metered against. Limits apply per rolling 24-hour window, so waiting frees budget as the oldest counted hour ages out (`Retry-After: 3600`). Upgrade your tier via support, or check remaining budget ahead of time with the `X-RateLimit-*` headers or [`GET /usage`](/interfaces/rest-api#check-your-usage). The stable `reason_code` remains `daily_quota_exceeded` for backward compatibility.

<Note>
  `generate` currently runs in report-only mode: over-limit generation requests are recorded but not rejected while the ceiling is tuned. The other classes (`decide`, `author`, `read`, `keys`, `admin`) are enforced.
</Note>

### `rate_limit_backend_unavailable` — 503

```json theme={null}
{
  "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, contact [eng@aethis.ai](mailto:eng@aethis.ai) 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:

```json theme={null}
{ "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](/concepts/nomenclature#ruleset_id-vs-rulebook_id-on-decide).
* **Ruleset is private.** Anonymous `/decide` only returns public rulesets; private rulesets 404 from an unauthenticated caller to avoid leaking their existence.
* **Slug typo on `/explain-failure`.** `/explain-failure` resolves a slug the same way `/decide`, `/schema`, and `/explain` do — pass either the friendly slug (`aethis/uk-fsm/child-eligibility`) or the concrete `ruleset_id` from your `/decide` envelope. A 404 here is a spelling/visibility issue, not a slug-vs-id issue.

***

## 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:

```json theme={null}
{
  "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](/recipes/author-a-rule#composing-sections-into-a-rulebook).

### `extra_forbidden` — an undefined top-level request key

Request bodies are strict. A top-level key the API does not define is
**rejected**, not ignored:

```json theme={null}
{
  "detail": [
    {
      "type": "extra_forbidden",
      "loc": ["body", "batch"],
      "msg": "Extra inputs are not permitted"
    }
  ]
}
```

If you have client code sending a field that is not in the
[API reference](/api-reference/introduction), it was never doing what you
thought — the server was not implementing it silently.

### `ruleset_record_corrupt` — 422 on a direct read

A stored ruleset that fails validation when read is reported as a structured
`422` on a direct lookup, rather than surfacing as a page-wide `500`. On a
catalogue listing the same record is skipped instead, and the count is
reported in the `X-Aethis-Records-Omitted` response header — read that header
before concluding a short page means the end of the catalogue. See
[Honest catalogue counts](/interfaces/rest-api#honest-catalogue-counts).

***

## Date field values

`Date` fields are stored internally as integer ordinals (days since year 1). Since aethis-core **0.31.0**, `/decide` and `/explain-failure` also accept strict ISO date strings on date fields and convert server-side — `"2025-04-13"` and `739354` are equivalent inputs.

Two caveats:

* **Bare `YYYY-MM-DD` only.** ISO datetimes (`"2025-04-13T00:00:00Z"`) are not accepted.
* **Rulebook evaluations still require ordinals.** ISO acceptance applies when evaluating a ruleset (`ruleset_id`); the `rulebook_id` path takes integer ordinals only.

A date value that parses as neither form (`"2026-02-30"`, `"next tuesday"`) does not fail the request: `/decide` returns 200 with the field named in `field_errors` and that answer ignored, so the decision is computed from the remaining fields (typically `undetermined`).

To convert an ISO date to an ordinal yourself:

```bash theme={null}
# Python
python3 -c "from datetime import date; print(date(2025, 4, 13).toordinal())"
# → 739354
```

```javascript theme={null}
// JavaScript
Math.floor(new Date('2025-04-13').getTime() / 86400000) + 719163
// → 739354
```

***

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


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.