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

# API reference

> Eighty operations across twenty families, one base URL, one error format, and the conventions every route shares.

Everything Exo can do is here: 64 paths, 80 operations, 135 named schemas. This page covers what is true of all of them, so the family pages can get straight to what is true of theirs.

<Note>
  The API is in private preview. The routes and payloads on this site are the committed v1 contract. Keys are issued to design partners while the public rollout finishes, so there is no live playground here yet.
</Note>

## Base URL

```
https://api.get-exo.com
```

Every path in this reference is absolute from that host and begins with `/v1`. There is no other version prefix, and no shipped route is ever renamed. See [Versioning](/platform/versioning).

## Authentication

Send an Exo API key on every request, as a header or as a bearer token. The two forms are equivalent.

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  curl https://api.get-exo.com/v1/me \
    -H "X-Exo-API-Key: $EXO_KEY"
  ```

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

  response = httpx.get(
      "https://api.get-exo.com/v1/me",
      headers={"X-Exo-API-Key": EXO_KEY},
  )
  response.raise_for_status()
  print(response.json())
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  const response = await fetch("https://api.get-exo.com/v1/me", {
    headers: { "X-Exo-API-Key": process.env.EXO_KEY! },
  });
  if (!response.ok) throw new Error(`${response.status}`);
  console.log(await response.json());
  ```
</CodeGroup>

Keys look like `exo_<env>_<token>`. They are minted at [`POST /v1/keys`](/api-reference/keys/create-key), shown exactly once, and revoked at [`DELETE /v1/keys/{key_id}`](/api-reference/keys/delete-key). Only the SHA-256 hash is stored, and that hash is the `id` you list and revoke by.

A missing or unrecognised credential is 401 [`authentication_error`](/errors/authentication_error). A valid credential without the required scope is 403 [`permission_denied`](/errors/permission_denied), and the problem body names the exact scope required.

[`GET /v1/me`](/api-reference/meta/get-me) is the right first call with a new key: it needs no data and no scope, so it answers whether the credential works before anything else can confuse the question.

## Wire format

**Request and response bodies are JSON with camelCase field names.** `nodeId`, `topK`, `degradationReason`, `subjectDefault`. Some request bodies also accept snake\_case for the same field, but camelCase is the documented form and the one every response uses.

Path parameters appear as the contract declares them, which is snake\_case: `/v1/graph/nodes/{node_id}`. That is a URL template, not a body field.

Three conventions follow:

* **Timestamps are RFC 3339 in UTC**, for example `2026-07-14T09:12:03Z`.
* **Unknown fields are additive, not errors.** New fields can appear inside an existing object at any time. Read tolerantly: ignore keys you do not know rather than failing on them.
* **Enumerated values are documented as strings, not closed enums**, wherever a future internal value could appear. Connector `status` and job `status` are both like this.

## Three headers the schema tables do not show

The generated request tables on each endpoint page come from the OpenAPI document, and the document does not declare these as parameters. They are also the conventions most likely to bite, so every endpoint page states by hand which of them apply to that route.

| Header            | Direction | What it does                                                          |
| ----------------- | --------- | --------------------------------------------------------------------- |
| `X-Exo-Subject`   | Request   | Acts for a provisioned subject instead of the key owner               |
| `Idempotency-Key` | Request   | Makes a retry of a designated POST safe                               |
| `X-Request-Id`    | Response  | Identifies the request in support, on every response including `204`s |

### `X-Exo-Subject`

A **subject** is one end user of your product, with their own isolated memory partition. Provision one with [`PUT /v1/subjects/{subject_id}`](/api-reference/subjects/upsert-subject), then act as them by adding one header:

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
curl -X POST https://api.get-exo.com/v1/retrieve \
  -H "X-Exo-API-Key: $EXO_KEY" \
  -H "X-Exo-Subject: customer_6412" \
  -H "Content-Type: application/json" \
  -d '{"query": "What did this customer decide about pricing?"}'
```

Subject selection is a header, not a body field. Some JSON-bodied routes also read a top-level `subject` field; the header is the documented selector because it works everywhere, including on GETs. Omit it and the call acts as the key owner, so single-user integrations stay one line.

Routes fall into three groups, and every endpoint page says which one it is in:

| Behaviour     | What it means                                                                                                                                                                | Where                                                                                                           |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| Selects       | The header routes the call. An unprovisioned id is 403 [`subject_not_provisioned`](/errors/subject_not_provisioned).                                                         | Retrieval, search, ingestion, recall, graph, condition, contradictions and proposals, settings, export creation |
| Refuses       | The route scopes to the key owner and rejects a selector with 400 [`subject_not_supported`](/errors/subject_not_supported) rather than silently reading the wrong partition. | Brain, coding sessions                                                                                          |
| Not consulted | The route is organization-level or acts on the caller.                                                                                                                       | Account, keys, jobs, team, usage, connectors, webhook management, events                                        |

Two rules worth knowing. **Provisioning is the privileged act, not selection**: creating or erasing a subject needs `subjects:provision`, while acting as one needs no extra scope, because any read or write key in the organization has always been able to route to a provisioned partition. And **a sandbox key cannot select out of its own partition**: it gets 403 [`permission_denied`](/errors/permission_denied) if it tries.

`subjectDefault` on `GET /v1/me` is the key owner's internal user id, which is what a request acts as when no selector is sent. It is not a value you can send back as a selector: internal ids never leave the API in that role.

See [One memory per end user](/guides/subjects).

### `Idempotency-Key`

Send any unique string. On a route that honours it, retrying with the same key and the same body replays the original response instead of acting a second time.

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
curl -X POST https://api.get-exo.com/v1/ingest \
  -H "X-Exo-API-Key: $EXO_KEY" \
  -H "Idempotency-Key: 8f14e45fceea167a" \
  -H "Content-Type: application/json" \
  -d '{"content": "We dropped the freemium tier."}'
```

Twelve POST routes honour it. Every other route ignores it, and each endpoint page says which. Three rules catch people out:

* **A key reused with a different body is 422** [`idempotency_key_reuse`](/errors/idempotency_key_reuse). A key identifies one request, not one caller.
* **A show-once secret comes back redacted on a replay**, never in the clear. A minted key's plaintext and a webhook's signing secret exist on the original response only.
* **A validation failure does not consume the key.** The corrected request can reuse it.

Full detail, including the exact list of routes and the reason each excluded POST is excluded, is on [Idempotency](/platform/idempotency).

### `X-Request-Id`

Every response carries one, including `204`s and every error body, where it also appears as `requestId`. It is the value to quote when something looks wrong. See [Request IDs](/platform/request-ids).

## Errors

Every failure is `application/problem+json` following RFC 9457.

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "type": "https://docs.get-exo.com/errors/permission_denied",
  "title": "Permission denied",
  "status": 403,
  "detail": "This operation requires the 'memory:forget' scope. A key carrying 'write' also qualifies.",
  "code": "permission_denied",
  "requestId": "req_9f2c41ab7d0e5c18",
  "suggestedAction": "Use an API key that carries the 'memory:forget' scope (or 'write')."
}
```

`code` is the stable machine value to branch on, and it is always the tail of the `type` URI, which resolves to that code's page in this documentation. `detail` is written for a human reading a log and names the specific id or field involved. `suggestedAction`, `documentationUrl` and `errors[]` are optional and appear where they help.

The API returns 200, 201, 202, 204, 400, 401, 403, 404, 409, 422, 429 and 503. **There is no 500 in the contract by design**: not one of the 80 operations declares one. An unreachable dependency is 503 [`service_degraded`](/errors/service_degraded), which is retryable, rather than an opaque server error. See [Errors](/platform/errors) for the full catalogue.

## Scopes

A route declares the finest scope it needs, and any broader scope that already reached it still does:

* `write` grants `graph:write` and `memory:forget`
* `admin` grants `subjects:provision` and `usage:read`
* `memory:forget` grants `graph:write`

`graph:write` does **not** grant `memory:forget`. That asymmetry is the point: a key minted for an integration that edits the graph cannot destroy content. Every endpoint page names the scope it requires and every scope that also satisfies it. See [Scopes](/scopes).

## Conventions every route shares

| Convention                           | The short version                                                                         |
| ------------------------------------ | ----------------------------------------------------------------------------------------- |
| [Request IDs](/platform/request-ids) | `X-Request-Id` on every response, including 204s. Quote it in support.                    |
| [Rate limits](/platform/rate-limits) | Per credential, per family. Read `RateLimit`, honour `Retry-After`.                       |
| [Pagination](/platform/pagination)   | Opaque keyset cursors: `{ data, hasMore, nextCursor }`, `pageSize` clamped into 1 to 100. |
| [Idempotency](/platform/idempotency) | `Idempotency-Key` on the designated POSTs makes a retry replay instead of reapply.        |
| [Events](/platform/events)           | An SSE stream and signed webhooks, carrying the same catalogue.                           |
| [Webhooks](/platform/webhooks)       | Registration, the signature recipe, retries and auto-disable.                             |
| [Versioning](/platform/versioning)   | `/v1` evolves additively. Retirements are announced with `Deprecation` and `Sunset`.      |
| [Sandbox](/platform/sandbox)         | `exo_sandbox_` keys act in a throwaway partition you can wipe.                            |

Asynchronous work is the other shared shape: ingestion, imports, exports and brain training all return a job id immediately. A `202` means the work is durably queued, not finished. Poll [`GET /v1/jobs/{job_id}`](/api-reference/jobs/get-job), or subscribe to `job.completed`.

## The families

<Columns cols={2}>
  <Card title="Retrieval" icon="crosshair" href="/api-reference/retrieve">
    The flagship. Ranked sources, contradictions, the reasoning behind the answer, and an honest degradation flag.
  </Card>

  <Card title="Search" icon="search" href="/api-reference/search">
    Hybrid vector and keyword search with no reasoning layer. The cheap primitive.
  </Card>

  <Card title="Ingestion" icon="upload" href="/api-reference/ingest">
    Put text, batches, files and transcripts into the graph. Every route returns a job id.
  </Card>

  <Card title="Jobs" icon="list-checks" href="/api-reference/jobs">
    Watch and cancel the background work every ingest queues.
  </Card>

  <Card title="Subjects" icon="users" href="/api-reference/subjects">
    Provision and erase one isolated memory per end user of your product.
  </Card>

  <Card title="Recall" icon="brain" href="/api-reference/recall">
    Structured reads of identity, graph, insights and temporal phases.
  </Card>

  <Card title="Graph" icon="workflow" href="/api-reference/graph">
    Read and mutate nodes, edges and domains, with an audit trail behind every write.
  </Card>

  <Card title="Contradictions and proposals" icon="split" href="/api-reference/insights">
    Belief conflicts Exo found, and connections it proposes rather than assumes.
  </Card>

  <Card title="Condition" icon="wand" href="/api-reference/condition">
    Turn an identity into a prompt any model you run can use.
  </Card>

  <Card title="Brain" icon="cpu" href="/api-reference/brain">
    Query, inspect and retrain the per-organization model over your graph.
  </Card>

  <Card title="Coding sessions" icon="terminal" href="/api-reference/sessions">
    Stream coding-agent transcripts in, then finalize to build the layers on top.
  </Card>

  <Card title="Connectors" icon="plug" href="/api-reference/connectors">
    Connect an external source once and let Exo keep pulling. GitHub in v1.
  </Card>

  <Card title="Events and webhooks" icon="radio" href="/api-reference/events">
    A live SSE stream for a session, and signed deliveries for a system.
  </Card>

  <Card title="Identity" icon="id-card" href="/api-reference/meta">
    Introspect the credential you are holding.
  </Card>

  <Card title="Keys and sandbox" icon="key" href="/api-reference/keys">
    Mint, list and revoke credentials, including disposable sandbox keys.
  </Card>

  <Card title="Usage" icon="chart-column" href="/api-reference/usage">
    Bucketed metering you can reconcile against your own billing.
  </Card>

  <Card title="Settings" icon="sliders-horizontal" href="/api-reference/settings">
    Per-subject preferences, read, patched and reset.
  </Card>

  <Card title="Exports" icon="package" href="/api-reference/exports">
    Take everything out as JSON, through an authenticated download.
  </Card>

  <Card title="Team" icon="building" href="/api-reference/team">
    The organization profile, and who is in it.
  </Card>

  <Card title="Account and purge" icon="triangle-alert" href="/api-reference/admin">
    The two destructive operations, both behind a confirmation ceremony.
  </Card>
</Columns>

## There is no SDK yet

Exo does not publish a client library. Rather than point you at a package that does not exist, here is what does.

**The OpenAPI document.** The full contract is served at `https://docs.get-exo.com/spec/exo-v1.json`: 64 paths, 80 operations, 135 named schemas, and an `x-exo-concepts` index naming the seven objects that make this API different. Generate a typed client from it with whatever generator your stack already uses. Because no shipped route is ever renamed and `/v1` evolves additively, a client generated today keeps working. See [Clients and SDKs](/platform/sdks).

**The MCP server.** If you are building inside a coding agent, connect Exo over MCP instead of writing HTTP calls yourself. See [Integrations](/integrations).

**These docs, as context.** Every page here is available as Markdown by appending `.md` to its URL, and the whole site is exposed as an MCP server:

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
claude mcp add --transport http exo-docs https://docs.get-exo.com/mcp
```

See [Read these docs from your agent](/integrations/docs-mcp).

When a client library does ship it will be generated from the same document, so nothing you build against the contract now becomes throwaway.
