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

# Errors

> One error format across all 80 operations: RFC 9457 problem+json with a stable machine code, and a page behind every code.

**Every failure the Exo API returns is `application/problem+json` following RFC 9457, and every one carries a stable snake\_case `code` you can branch on.**

There is no second error shape. Auth failures, validation failures, rate limits and dependency outages all arrive in the same envelope, from every route.

## The envelope

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "type": "https://docs.get-exo.com/errors/subject_not_provisioned",
  "title": "Subject not provisioned",
  "status": 403,
  "detail": "Subject 'customer_6412' is not provisioned for this organization.",
  "code": "subject_not_provisioned",
  "requestId": "req_9f2c41ab7d0e5c18",
  "suggestedAction": "Provision the subject first with PUT /v1/subjects/{subjectId}, or omit the subject selector to act as the key owner."
}
```

| Field              | Type    | Always present | What it is                                                                                                                    |
| ------------------ | ------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `type`             | string  | yes            | A URI identifying the error class. Always `https://docs.get-exo.com/errors/<code>`, and it resolves to that code's page here. |
| `title`            | string  | yes            | A short human title for the class of error. Stable per code.                                                                  |
| `status`           | integer | yes            | The HTTP status, repeated in the body so a logged payload is self-describing.                                                 |
| `detail`           | string  | yes            | What went wrong on this specific call, written for a person reading a log.                                                    |
| `code`             | string  | yes            | The stable machine value. **Branch on this.**                                                                                 |
| `requestId`        | string  | yes            | Echo of the `X-Request-Id` header. Quote it in support.                                                                       |
| `suggestedAction`  | string  | no             | What to do about it, in one sentence.                                                                                         |
| `documentationUrl` | string  | no             | A specific page for this failure, where a route sets one.                                                                     |
| `errors`           | array   | no             | Field-level violations. Present on `validation_error`.                                                                        |

**The invariant worth building on: `code` always equals the tail of the `type` URI.** Read either one, never both.

`errors[]` entries have a fixed shape:

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "pointer": "#/body/topK",
  "field": "topK",
  "code": "less_than_equal",
  "message": "Input should be less than or equal to 50"
}
```

`errors[].code` is a **field-level** code and is a different vocabulary from the top-level `code`. See [`validation_error`](/errors/validation_error) for the full list.

## Handling errors

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  curl -sS -X POST https://api.get-exo.com/v1/retrieve \
    -H "X-Exo-API-Key: $EXO_KEY" \
    -H "Content-Type: application/json" \
    -d '{"query": "Where did we land on pricing?"}' \
    -o body.json -w '%{http_code}\n'
  # on any non-2xx, body.json is the problem document
  jq -r '.code, .detail, .requestId' body.json
  ```

  ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  import httpx

  response = httpx.post(
      "https://api.get-exo.com/v1/retrieve",
      headers={"X-Exo-API-Key": EXO_KEY},
      json={"query": "Where did we land on pricing?"},
  )

  if response.is_error:
      problem = response.json()
      if problem["code"] == "rate_limit_exceeded":
          retry_after = int(response.headers.get("Retry-After", "1"))
          ...  # back off, then retry
      raise RuntimeError(
          f"{problem['code']}: {problem['detail']} ({problem['requestId']})"
      )
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  const response = await fetch("https://api.get-exo.com/v1/retrieve", {
    method: "POST",
    headers: {
      "X-Exo-API-Key": process.env.EXO_KEY!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ query: "Where did we land on pricing?" }),
  });

  if (!response.ok) {
    const problem = await response.json();
    if (problem.code === "rate_limit_exceeded") {
      const retryAfter = Number(response.headers.get("Retry-After") ?? 1);
      // back off, then retry
    }
    throw new Error(`${problem.code}: ${problem.detail} (${problem.requestId})`);
  }
  ```
</CodeGroup>

## Statuses the API returns

| Status | Meaning                                                   | Retry                   |
| ------ | --------------------------------------------------------- | ----------------------- |
| `200`  | Success with a body.                                      |                         |
| `201`  | A resource was created.                                   |                         |
| `202`  | Accepted and queued. Read the job id from the body.       |                         |
| `204`  | Success with no body. Headers still carry `X-Request-Id`. |                         |
| `400`  | The request could not be acted on as sent.                | No, fix it              |
| `401`  | No credential, or one that could not be validated.        | No, fix it              |
| `403`  | Valid credential, insufficient scope or subject access.   | No, fix it              |
| `404`  | No such resource for this caller.                         | No                      |
| `409`  | Well formed, but the current state refuses it.            | Depends on the code     |
| `422`  | Understood, but failed validation. See `errors[]`.        | No, fix it              |
| `429`  | Rate limited. Honour `Retry-After`.                       | Yes, after the interval |
| `503`  | A dependency is temporarily unavailable.                  | Yes, with backoff       |

**There is no 500 in the contract by design.** Not one of the 80 published operations declares a 500 response. A failure Exo can anticipate is a 4xx; a dependency Exo cannot reach is a 503, which tells a client to retry rather than to give up. The [`internal_error`](/errors/internal_error) page exists so the `type` URI resolves if one ever escapes.

## The code catalogue

Every code below has a page, because the `type` URI on the wire points at it.

### 400 Bad Request

| Code                                                       | When                                                            |
| ---------------------------------------------------------- | --------------------------------------------------------------- |
| [`invalid_request`](/errors/invalid_request)               | The default 400, where a route names nothing more specific.     |
| [`invalid_cursor`](/errors/invalid_cursor)                 | A pagination cursor is malformed or was not issued by this API. |
| [`pagination_unsupported`](/errors/pagination_unsupported) | A recall view that is not a list was sent a cursor.             |
| [`subject_not_supported`](/errors/subject_not_supported)   | `X-Exo-Subject` was sent to a route that does not accept it.    |

### 401 Unauthorized

| Code                                                   | When                                               |
| ------------------------------------------------------ | -------------------------------------------------- |
| [`authentication_error`](/errors/authentication_error) | Missing, malformed, revoked or expired credential. |

### 403 Forbidden

| Code                                                         | When                                                                                    |
| ------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
| [`permission_denied`](/errors/permission_denied)             | The credential lacks the required scope, or a sandbox key tried to leave its partition. |
| [`subject_not_provisioned`](/errors/subject_not_provisioned) | `X-Exo-Subject` names a subject this organization never provisioned.                    |

### 404 Not Found

| Code                                                 | When                                                        |
| ---------------------------------------------------- | ----------------------------------------------------------- |
| [`not_found`](/errors/not_found)                     | The default 404, where a route names nothing more specific. |
| [`node_not_found`](/errors/node_not_found)           | No such graph node for the effective subject.               |
| [`edge_not_found`](/errors/edge_not_found)           | No such graph edge for the effective subject.               |
| [`domain_not_found`](/errors/domain_not_found)       | No such graph domain for the effective subject.             |
| [`job_not_found`](/errors/job_not_found)             | No such job for this caller.                                |
| [`export_not_found`](/errors/export_not_found)       | No such export in this organization.                        |
| [`proposal_not_found`](/errors/proposal_not_found)   | No such edge proposal, or it was already decided.           |
| [`webhook_not_found`](/errors/webhook_not_found)     | No such webhook endpoint in this organization.              |
| [`connector_not_found`](/errors/connector_not_found) | No such connector installation to act on.                   |
| [`subject_not_found`](/errors/subject_not_found)     | The subject named in the body does not exist.               |
| [`key_not_found`](/errors/key_not_found)             | No such API key for this caller.                            |

### 409 Conflict

| Code                                                           | When                                                                            |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| [`conflict`](/errors/conflict)                                 | The default 409. Raised directly when a subject id collides with the key owner. |
| [`job_not_cancelable`](/errors/job_not_cancelable)             | The job has already left the pending state.                                     |
| [`export_not_ready`](/errors/export_not_ready)                 | The export has not finished successfully yet.                                   |
| [`webhook_limit_reached`](/errors/webhook_limit_reached)       | The organization is at its 20-endpoint cap.                                     |
| [`connector_not_syncable`](/errors/connector_not_syncable)     | The connector is not in an active state.                                        |
| [`purge_impact_changed`](/errors/purge_impact_changed)         | The data moved between the purge dry run and the confirm.                       |
| [`subject_mapping_invalid`](/errors/subject_mapping_invalid)   | The subject's internal mapping is not in a state Exo will erase from.           |
| [`sandbox_subject_conflict`](/errors/sandbox_subject_conflict) | The reserved sandbox subject id is taken by a regular subject.                  |
| [`idempotency_in_flight`](/errors/idempotency_in_flight)       | A request with this `Idempotency-Key` is still running.                         |

### 422 Unprocessable Content

| Code                                                     | When                                                       |
| -------------------------------------------------------- | ---------------------------------------------------------- |
| [`validation_error`](/errors/validation_error)           | One or more fields failed validation. Carries `errors[]`.  |
| [`invalid_expand`](/errors/invalid_expand)               | An `expand` value is not a relation this route can expand. |
| [`confirm_token_invalid`](/errors/confirm_token_invalid) | The purge confirm token is missing, malformed or expired.  |
| [`idempotency_key_reuse`](/errors/idempotency_key_reuse) | The key was already used with a different request body.    |

### 429 Too Many Requests

| Code                                                 | When                                                          |
| ---------------------------------------------------- | ------------------------------------------------------------- |
| [`rate_limit_exceeded`](/errors/rate_limit_exceeded) | The token bucket for this family of operations is empty.      |
| [`too_many_streams`](/errors/too_many_streams)       | The organization already has 5 concurrent event streams open. |

### 503 Service Unavailable

| Code                                           | When                                                    |
| ---------------------------------------------- | ------------------------------------------------------- |
| [`service_degraded`](/errors/service_degraded) | A dependency is temporarily unavailable. Safe to retry. |

### Reserved

These exist in the problem envelope's status map. No published operation declares them, and the pages exist only so the `type` URI resolves rather than returning a 404.

| Code                                       | Status |
| ------------------------------------------ | ------ |
| [`internal_error`](/errors/internal_error) | 500    |
| [`provider_error`](/errors/provider_error) | 502    |
| [`timeout`](/errors/timeout)               | 504    |

## Next

<Columns cols={2}>
  <Card title="Rate limits" icon="gauge" href="/platform/rate-limits">
    The families, their quotas, and how to read the RateLimit headers.
  </Card>

  <Card title="Request IDs" icon="fingerprint" href="/platform/request-ids">
    What `X-Request-Id` is for and how to quote it.
  </Card>
</Columns>
