> ## Documentation Index
> Fetch the complete documentation index at: https://docs.memory.anonalabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> The error envelope, the error codes the API raises, and how to retry safely.

## Error envelope

Every endpoint fails in the same shape:

```json theme={null}
{
  "error": {
    "code": "invalid_api_key",
    "message": "Invalid API key",
    "request_id": "req_abc123"
  }
}
```

| Field | Description |
| - | - |
| `code` | Stable machine-readable identifier. Branch on this, not on `message`. |
| `message` | Human-readable explanation. Wording may change. |
| `request_id` | Identifies the exact request server-side. Include it when contacting support. |

### Validation errors

When a request body fails schema validation, the `422 validation_error` carries one
more field inside `error`: `errors`, an array with one entry per failing field. The
example is a `POST /v1/retrieve` with no `query` and a `limit` of 500. `message` still describes only the first failure,
formatted `field: message`, so a form can use `errors` to mark every bad field in one
round trip.

```json theme={null}
{
  "error": {
    "code": "validation_error",
    "message": "query: Field required",
    "errors": [
      { "field": "query", "message": "Field required" },
      { "field": "limit", "message": "Input should be less than or equal to 100" }
    ],
    "request_id": "req_abc123"
  }
}
```

| Field | Description |
| - | - |
| `errors[].field` | Dotted path to the failing field, such as `items.0.content`. `request` when the failure is not tied to one field. |
| `errors[].message` | What is wrong with it. Wording may change. |

The rejected value itself is never echoed back, so a 422 cannot leak what was
submitted. `errors` is present only on schema-validation failures. Other codes, and the
few `validation_error` responses raised by checks made after the body has parsed, carry
`code` and `message` alone, so read `errors` when it is there and fall back to
`message`.

## Status and error codes

The codes you will actually see, by status:

| Status | Code | Meaning | Retryable |
| - | - | - | - |
| 400 | `org_mismatch` | The request names an organization (`X-Anona-Org` or `org_id`) other than the one its API key or session belongs to. Refused rather than served from the credential's organization. | No |
| 400 | `invalid_org_id` | `X-Anona-Org` was sent more than once or is not a UUID, or an MCP tool call's `org_id` argument is not a UUID. A malformed `org_id` in a request body is `422 validation_error`. | No |
| 401 | `missing_auth` | No `Authorization` header (and no `x-api-key`). | No |
| 401 | `invalid_api_key` | The key is not one Anona issued, or was revoked. | No |
| 401 | `key_expired` | The key was valid but passed its expiry date. Rotate it. | No |
| 401 | `invalid_token` | The dashboard session token is invalid or expired. | No |
| 401 | `session_required` | An API key or temporary-profile token was sent to a control-plane endpoint. Some of them also accept the `anona login --cli` token; see [which credential each route accepts](https://docs.memory.anonalabs.com/api-reference/authentication.md#which-credential-each-route-accepts). | No |
| 403 | `space_access_denied` | You are not a member of this space. | No |
| 403 | `space_read_only` | A read-only member attempted a write. | No |
| 403 | `space_owner_only` | A non-owner attempted export, memory delete/patch, document delete, or space config. | No |
| 403 | `temp_profile_single_space` | A temporary profile wrote to, or tried to create, a space other than its one space, `default`. Use `default` (or omit the space), or claim the profile to create more. | No |
| 403 | `space_unavailable` | The owner of a shared space can't be reached: the owning organization has revoked its API keys and holds no live one, or was deleted. A share is served while the owner holds a live key, and also when it has never created a key or holds only expired ones. | No |
| 403 | `not_an_org_member` | A signed-in (OAuth or CLI) caller named an organization (`org_id` / `X-Anona-Org`) they are not a member of, or one that was deleted. | No |
| 403 | `integration_owner_only` | Installing or disconnecting a Slack or GitHub integration is owner-only, and binding, unbinding and backfill are owner or admin (`slack_admin_only` / `github_admin_only`), once shared organizations are enabled. Until then owners and admins install and disconnect (others get `slack_admin_only` / `github_admin_only`), and any member can bind, unbind and backfill. | No |
| 403 | `insufficient_role` | Your role cannot perform this control-plane action. | No |
| 403 | `org_suspended` | The organization is suspended. Also returned when an MCP or CLI caller selects a suspended organization. | No |
| 403 | `no_api_key` | Your organization holds no live API key, so the request cannot be served. Reached by a session, MCP OAuth or CLI caller on a route that forwards a key to the memory engine itself; the usual cause is that every key was revoked. Create one. | No |
| 404 | `space_not_found` | The space does not exist, or the id is misspelled. | No |
| 400 | `memory_not_editable` | The memory is a note, synthesised from other memories, and cannot be edited directly. Edit the facts it was built from (its `source_ids`); the note is rebuilt from them. | No |
| 404 | `memory_not_found` | The space exists but holds no memory with that id — a misspelled id, or one already deleted. Returned by memory delete, edit and history alike. | No |
| 404 | `job_not_found` | The space holds no job with that id, on reading or cancelling a job. The id belongs to the space it was queued in. | No |
| 409 | `space_ambiguous` | A bare space name matches both your own space and a shared one — use the `qualified_id`. | No |
| 409 | `job_not_cancellable` | `DELETE /v1/spaces/{space_id}/jobs/{job_id}` on a job that cannot be stopped: it has already finished, or its parts are already running. The message says which. | No |
| 422 | `validation_error` | The body fails schema validation. Unknown fields are rejected too, so a misspelled field name lands here rather than being silently dropped. | No |
| 422 | `reserved_tag` | A tag starting with the reserved `anona:` prefix. | No |
| 422 | `invalid_scope` | A `user_id`, `agent_id` or `session_id` that is empty, or is not 1-128 characters of letters, digits or `._@=+-` (no spaces, no colons). | No |
| 422 | `reserved_metadata_key` | A `metadata` key starting with the reserved `anona_` prefix. | No |
| 422 | `route_conflict` | `space_id` and `route` were both supplied, or neither was — or, on `/v1/retrieve`, `max_targets` was sent without `route`. Applies to `/v1/record`, `/v1/record/batch` and `/v1/retrieve`. | No |
| 422 | `no_route_target` | `route: "auto"` had nowhere to go. On a write: no eligible space and no usable default. On a read: no space could be selected for a reason that is not an answer about your memories, and no `fallback_space_id` was sent. A routed read that simply fits no space is not this error, it is an empty `200`. See [Automatic space routing](https://docs.memory.anonalabs.com/guides/automatic-routing.md#when-no-space-fits). | No |
| 429 | `rate_limited` | The per-minute rate ceiling, on metered endpoints. Clears on its own. | Yes, with backoff |
| 429 | `credits_exhausted` | The plan allowance and any purchased top-up credits are both spent. Backoff will not help — upgrade, buy a credit top-up, or wait for the reset. | No |
| 500 | `internal_error` | Unexpected server error. | Yes |
| 4xx or 5xx | `memory_error` | The memory engine refused or failed a call and no more specific code applies — the engine's own status is passed through, so this code appears on several. It is the default engine failure on `/v1/record`, `/v1/retrieve` and the memory routes; some surfaces replace it with their own (see below). | Only on a `5xx` |
| 503 | `service_unavailable` | Temporary degradation. | Yes, after a short delay |

A handful of framework-level failures fall back to generic codes — `bad_request`,
`unauthorized`, `forbidden`, `not_found`, `method_not_allowed`, `conflict`,
`rate_limited`, `internal_error`, `service_unavailable`. These come from the web
framework rather than from a route, so in practice you see them only for a request
that reached no endpoint: a misspelled path is `404 not_found`, the wrong verb on a
real path is `405 method_not_allowed`. **No documented endpoint returns
`unauthorized` or `forbidden`** — every authentication and authorization refusal
carries one of the specific codes above (`missing_auth`, `invalid_token`,
`invalid_api_key`, `key_expired`, `no_api_key`, `space_access_denied`, and so on).
Branch on the specific codes and treat the generic ones as "my request did not reach
the API I meant".

This table is the canonical list. Where another page in these docs shows an error, it
shows a code from here; if you find one that does not, the other page is wrong and we
want to hear about it.

### Endpoint-specific codes

| Status | Code | Where | Meaning |
| - | - | - | - |
| 400 | `unsupported_model` | `/v1/reason`, proxy endpoints, `POST /v1/spaces/{id}/graph`, model settings | `model` is not one this deployment serves. The message suggests up to three close matches and points to `GET /v1/models` for the full list; see [model selection](https://docs.memory.anonalabs.com/api-reference/model-selection.md). |
| 429 | `model_throttled` | `/v1/reason`, proxy endpoints | The chosen model is at capacity. Request limits are per model, so another model may have headroom. Anona never silently substitutes one. |
| 503 | `model_unavailable` | `/v1/reason`, proxy endpoints | The model is catalogued but this deployment cannot invoke it. Ours to fix, not yours. |
| 400 | `invalid_model` | `/v1/chat/completions`, `POST /v1/spaces/{id}/graph` | A model this deployment serves was rejected by the provider itself. Distinct from `unsupported_model`, which is refused before the call is made. |
| 400 | `invalid_tunable` | proxy endpoints | An `X-Anona-*` tunable header could not be parsed. The message names the header and what it expected. |
| 401 | `key_expired` | any authenticated endpoint | The key was valid but has passed its expiry date. Distinct from `invalid_api_key`: this key worked yesterday, so the fix is to rotate rather than to go looking for the right key. Expiry cannot be extended. |
| 401 | `session_required` | every control-plane endpoint — exports, space members, API keys, org, usage, billing, invitations | The endpoint authenticates with a signed-in dashboard session, not an API key or temporary profile. The reads listed under [which credential each route accepts](https://docs.memory.anonalabs.com/api-reference/authentication.md#which-credential-each-route-accepts), plus export start and invitation accept/decline, also accept the `anona login --cli` token. The key you sent may be perfectly valid — it is the wrong kind of credential here. |
| 429 | `credits_exhausted` | metered endpoints | You have used all your credits: the monthly plan allowance and any purchased top-up credits are both spent. Upgrade your plan or buy a credit top-up to continue. Distinct from `rate_limited`, which is a per-minute ceiling and clears on its own. |
| 403 | `topup_requires_paid_plan` | `POST /v1/billing/topup` | Credit packs are for organizations on a paid plan. A free Developer organization has to [upgrade](https://docs.memory.anonalabs.com/api-reference/billing.md#change-plan) first. |
| 503 | `space_list_unavailable` | `GET /v1/spaces` | Space access could not be determined. Retry; the list is never returned unfiltered. |
| 503 | `reason_timeout` | `/v1/reason`, the MCP `reason` tool | The question took longer to answer over this space than the deadline allows. Not a crash and not your request being rejected: ask something more specific, or — on `depth: "thorough"` — retry at `depth: "fast"`, which the message names. |
| 4xx or 5xx | `graph_error` | `/v1/spaces/{id}/graph`, `/entities`, `/entities/{entity_id}` | The engine refused or failed a graph call, with its status passed through. A missing entity id is `404 graph_error`, **not** `space_not_found` — that code means the space itself does not exist. |
| 4xx or 5xx | `model_error` | `/v1/spaces/{id}/models`, `/profile` and their sub-routes | The engine refused or failed a memory-model call, with its status passed through. A memory model that does not exist is `404 model_error`; so is a missing space on the memory-model routes, while `GET /v1/spaces/{id}/profile` on a missing space is `404 space_not_found`. Unrelated to `unsupported_model` / `invalid_model`, which are about the LLM. |
| 4xx or 5xx | `document_error` | `POST /v1/spaces/{id}/documents` and the document routes | The engine refused or failed a document call, with its status passed through. |
| 4xx or 5xx | `webhook_error` | the webhook routes | The engine refused or failed a webhook call, with its status passed through. |
| 503 | `write_status_unknown` | the synchronous write endpoints — `/v1/record`, `/v1/record/batch`, document upload | The write timed out against the engine. Unlike every other failure here it does **not** mean the write was rejected — it may have landed. Read the space back before retrying, or a retry can double-write. |
| 503 | `delete_unavailable` | `DELETE /v1/spaces/{space_id}/memories/{memory_id}` | Deletion is briefly unavailable. The memory has **not** been deleted — the only case where this is returned is one where we cannot confirm a delete, and we will not report one we did not make. Retry. |

Some endpoints add further codes. See [Documents](https://docs.memory.anonalabs.com/api-reference/documents.md#error-responses),
[Webhooks](https://docs.memory.anonalabs.com/api-reference/webhooks.md#errors), [Organization](https://docs.memory.anonalabs.com/api-reference/organization.md#error-responses),
[Context Receipts](https://docs.memory.anonalabs.com/api-reference/context-receipts.md#error-responses), and
[Billing](https://docs.memory.anonalabs.com/api-reference/billing.md#error-responses).

## Common mistakes

```bash theme={null}
# Correct
Authorization: Bearer anona_live_YOUR_KEY

# Also accepted on every data-plane endpoint (how the Anthropic SDK sends keys)
x-api-key: anona_live_YOUR_KEY

# Rejected: the Bearer scheme is missing
Authorization: anona_live_YOUR_KEY
```

## Retrying

Retry `429`, `500` and `503` with backoff. Never retry a `4xx` other than `429`:
the request will fail identically every time. And never blindly retry a write on
a `5xx` — a `5xx` means the request may well have executed, so a re-sent
`POST /v1/record` stores the memory twice.

### What the SDKs already retry

Both SDKs retry on your behalf, but **not on every call**, and the two draw the
line differently. Check this table before adding a loop of your own.

| Call | Retried on `429`? | Retried on `5xx`? | Retried on a dropped connection or timeout? |
| - | - | - | - |
| `retrieve`, `get_context`, `retrieve_receipt` | both SDKs | **Python no**, TypeScript yes | Python no, TypeScript yes |
| `record`, `record_batch` | both SDKs | neither | neither |
| `reason`, `reason_receipt`, `ask_about_user` | both SDKs | neither | neither |
| File uploads | both SDKs | neither | neither |
| `create_space`, `create_webhook`, `create_rule`, `create_memory_model`, `refresh_memory_model` | both SDKs | neither | neither |
| Every `GET`, `PUT` and `DELETE` — reads, settings, deletes | both SDKs | both SDKs | Python no, TypeScript yes |

Three things explain the whole table:

* **A `429` is always retried, writes included.** The limiter refuses the
  request before the route runs, so nothing executed and a re-send cannot
  double-write. `credits_exhausted` is the one exception — it rides a `429` but
  is a balance, not a window, so no backoff can clear it and neither SDK retries
  it.
* **A `5xx` is only retried where a replay is safe.** The Python client decides
  by HTTP method: `GET`, `HEAD`, `OPTIONS`, `PUT` and `DELETE` are retried, and
  every `POST` is not. `retrieve` and `reason` are `POST` requests, so **Python
  does not retry them on a `5xx` even though they are reads** — the TypeScript
  client marks each create explicitly instead and so does retry `retrieve`. If
  you need a `5xx` retried on `retrieve` in Python, wrap that one call.
* **A multipart upload is never retried.** The body is built from your own file
  objects and they are not rewound between attempts, so a replay would upload a
  truncated file. Uploads surface the error instead.

### A loop that is safe to put above them

Only the first table row leaves a gap worth closing. Retry `AnonaError` on the
statuses the SDK skipped, honour the wait the server named, and let everything
else through untouched.

```python Python theme={null}
import random, time

from anona import AnonaError

_RETRYABLE = {429, 500, 503}


def retry_read(call, max_retries=3):
    """Retry a read the Python SDK leaves alone because it is a POST."""
    for attempt in range(max_retries + 1):
        try:
            return call()
        except AnonaError as e:
            last = attempt == max_retries
            if last or e.status_code not in _RETRYABLE:
                raise
            # A balance, not a window: no wait clears it.
            if e.code == "credits_exhausted":
                raise
            # The rate-limit window is a whole minute, so the server's own
            # number is the only backoff that can succeed.
            wait = e.retry_after or 2**attempt + random.uniform(0, 1)
            time.sleep(wait)


results = retry_read(
    lambda: client.retrieve(space_id="customer-support-bot", query="Where does Alice work?")
)
```

```typescript TypeScript theme={null}
import { AnonaError } from "@anona-labs/memory";

const RETRYABLE = new Set([429, 500, 503]);

async function retryRead<T>(call: () => Promise<T>, maxRetries = 3): Promise<T> {
  for (let attempt = 0; ; attempt++) {
    try {
      return await call();
    } catch (err) {
      const retryable =
        err instanceof AnonaError &&
        RETRYABLE.has(err.statusCode) &&
        err.code !== "credits_exhausted";
      if (!retryable || attempt === maxRetries) throw err;
      const waitMs = err.retryAfter ? err.retryAfter * 1000 : 2 ** attempt * 1000 + Math.random() * 1000;
      await new Promise((r) => setTimeout(r, waitMs));
    }
  }
}
```

Note what both versions do **not** do: they never catch a bare `Exception`, and
they never retry a `4xx`. A loop that catches everything retries
`validation_error` and `space_not_found` three times before reporting the same
failure three times more slowly.

Tuning the SDK's own retrying is usually enough, and it is one constructor
argument: `max_retries` / `maxRetries` (default `2`, `0` turns retrying off),
and in Python `retry_max_wait` (default `60.0` seconds) caps how long a single
hinted wait may park the call. See the
[Python SDK](https://docs.memory.anonalabs.com/sdk/python.md#client-initialization) and
[TypeScript SDK](https://docs.memory.anonalabs.com/sdk/javascript.md) pages.

## Handling errors in the SDKs

```python Python theme={null}
from anona import AnonaClient, AnonaError

client = AnonaClient(api_key="anona_live_YOUR_KEY")

try:
    results = client.retrieve(space_id="customer-support-bot", query="Where does Alice work?")
except AnonaError as e:
    print(e.status_code, e.code, e.request_id)
```

```typescript TypeScript theme={null}
import { Anona, AnonaError } from "@anona-labs/memory";

const anona = new Anona({ apiKey: "anona_live_YOUR_KEY" });

try {
  const results = await anona.retrieve({
    spaceId: "customer-support-bot",
    query: "Where does Alice work?",
  });
} catch (err) {
  if (err instanceof AnonaError) {
    console.log(err.statusCode, err.code, err.requestId);
  } else {
    throw err;
  }
}
```

The TypeScript class carries the same information under camelCase names
(`statusCode`, `requestId`), plus `retryAfter` on a `429`.

| Attribute | Type | Contents |
| - | - | - |
| `status_code` | `int` | The HTTP status. |
| `code` | `str` or `None` | The stable machine-readable code from the envelope. Branch on this. |
| `detail` | `dict` or `str` | The parsed error body, or the raw text when the response is not JSON. |
| `request_id` | `str` or `None` | The `X-Request-ID` of the failing call. Quote it to support. |
| `retry_after` | `int` or `None` | Seconds to wait, on a `429` that named a number. |

The TypeScript client raises the same error with `statusCode`, `code`,
`requestId`, `detail` and `retryAfter`. Both SDKs already retry `429` on every
call and a `5xx` on the calls where a replay is safe — see
[What the SDKs already retry](#what-the-sdks-already-retry) before adding a
second loop, because for most calls there is nothing left to add.

## Next steps

> **[API overview](https://docs.memory.anonalabs.com/api-reference/overview.md)**
>
> Every endpoint in one table.

> **[Support](mailto:support@anonalabs.com)**
>
> Contact us with a `request_id`.


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