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

# One memory per end user

> Provision a subject for each end user of your product, then select them with one header on every call.

A **subject** is one end user of your product, given their own isolated memory partition inside your organization. You provision a subject once, then any call can act as that subject by adding a single header. Omit the header and calls act as the API key owner, so single-user integrations never think about this at all.

## Before you start

<Columns cols={3}>
  <Card title="A key that can provision" icon="key" href="/scopes">
    `PUT` and `DELETE` on subjects need `subjects:provision`. An `admin` key also qualifies.
  </Card>

  <Card title="A stable id per user" icon="fingerprint" href="#choosing-subject-ids">
    1 to 128 characters of `A-Z a-z 0-9 . _ : -`. Your own user id works well.
  </Card>

  <Card title="Somewhere to store it" icon="database" href="/guides/ingesting">
    Your database maps your user to their subject id. Exo never returns your mapping.
  </Card>
</Columns>

## The shortest call that works

Provisioning takes no request body. The id in the path is the whole request.

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

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

  r = httpx.put(
      "https://api.get-exo.com/v1/subjects/customer_6412",
      headers={"X-Exo-API-Key": EXO_KEY},
  )
  print(r.status_code, r.json())
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  const r = await fetch("https://api.get-exo.com/v1/subjects/customer_6412", {
    method: "PUT",
    headers: { "X-Exo-API-Key": process.env.EXO_KEY! },
  });
  console.log(r.status, await r.json());
  ```
</CodeGroup>

```json 201 Created theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "id": "customer_6412",
  "createdAt": "2026-07-30T14:22:08Z"
}
```

The call is idempotent: **201 the first time, 200 every time after**, with the same body. That means you can call it on every login without checking whether the subject exists.

## Selecting a subject on a call

Send `X-Exo-Subject`. That header is the selector, on every route that takes one.

<CodeGroup>
  ```bash cURL 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 onboarding?"}'
  ```

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

  exo = httpx.Client(
      base_url="https://api.get-exo.com",
      headers={"X-Exo-API-Key": EXO_KEY},
  )

  def as_subject(subject_id: str) -> dict:
      return {"X-Exo-Subject": subject_id}

  r = exo.post(
      "/v1/retrieve",
      headers=as_subject("customer_6412"),
      json={"query": "What did this customer decide about onboarding?"},
  )
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  function exoHeaders(subjectId?: string): Record<string, string> {
    const h: Record<string, string> = {
      "X-Exo-API-Key": process.env.EXO_KEY!,
      "Content-Type": "application/json",
    };
    if (subjectId) h["X-Exo-Subject"] = subjectId;
    return h;
  }

  const r = await fetch("https://api.get-exo.com/v1/retrieve", {
    method: "POST",
    headers: exoHeaders("customer_6412"),
    body: JSON.stringify({ query: "What did this customer decide about onboarding?" }),
  });
  ```
</CodeGroup>

<Warning>
  Subject selection is a **header**, not a body field. Putting `subject` in a JSON body does not select a subject on the ingest, retrieve or condition routes, and no error tells you it was ignored: the call silently reads and writes the key owner's partition instead. If a call is returning the wrong person's data, check that the header is actually being sent.
</Warning>

Because the selector is a header, wrap it once in your client rather than threading it through every call site. Both snippets above show the shape worth copying.

## The lifecycle

<Steps>
  <Step title="Provision on first sight" icon="user-plus">
    Call `PUT /v1/subjects/{id}` when the end user first appears in your product, or on every login. It is idempotent, so there is no "already exists" branch to write.
  </Step>

  <Step title="Write and read as them" icon="arrow-left-right">
    Every ingest, import, retrieve, search, condition and recall call takes `X-Exo-Subject`. Their content lands in their partition and never mixes with another subject's.

    ```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 "X-Exo-Subject: customer_6412" \
      -H "Content-Type: application/json" \
      -d '{"content": "Prefers async review over live calls."}'
    ```
  </Step>

  <Step title="List and inspect" icon="list">
    `GET /v1/subjects` is keyset-paginated: page with `nextCursor` until `hasMore` is false. `pageSize` is coerced into 1 to 100 and defaults to 25.

    ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    curl "https://api.get-exo.com/v1/subjects?pageSize=50" \
      -H "X-Exo-API-Key: $EXO_KEY"
    ```

    ```json 200 OK theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    {
      "data": [
        { "id": "customer_6412", "createdAt": "2026-07-30T14:22:08Z" },
        { "id": "customer_7781", "createdAt": "2026-07-29T09:04:55Z" }
      ],
      "hasMore": false,
      "nextCursor": null
    }
    ```

    `GET /v1/subjects/{id}` fetches one, or returns 404 `subject_not_found`. Use it as an existence check.
  </Step>

  <Step title="Erase when they leave" icon="trash-2">
    `DELETE /v1/subjects/{id}` is the documented right-to-be-forgotten path. It returns 204 and is idempotent.

    ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    curl -X DELETE https://api.get-exo.com/v1/subjects/customer_6412 \
      -H "X-Exo-API-Key: $EXO_KEY" -i
    ```

    See [Forgetting and portability](/guides/forgetting) for exactly what it removes.
  </Step>
</Steps>

## Choosing subject ids

The id must be 1 to 128 characters drawn from `A-Za-z0-9._:-`. It is yours to choose and it is the only handle you get back, so pick something you already store.

| Good                                            | Why                                           |
| ----------------------------------------------- | --------------------------------------------- |
| `customer_6412`                                 | Your own primary key, prefixed                |
| `0d5b7c92-3a41-4e88-9b02-6f7c1d4a5e30:user_338` | Encodes your own tenancy in one stable string |
| `u_9f2c1e04`                                    | Opaque, stable, no personal data in the id    |

Avoid raw email addresses: `@` is not in the allowed character set, and an id is not the place for personal data you may later have to erase.

<Note>
  A subject id that collides with the key owner is rejected with 409. The key owner is not a subject; it is the identity a request acts as when no selector is sent.
</Note>

## What isolation actually means

Each organization gets its own physical database schema. Subjects live inside it, scoped per user. Two consequences worth designing around:

* **Cross-subject reads do not happen by accident.** A node belonging to another subject returns 404 rather than leaking its existence.
* **Any key in your organization can read any subject in it.** Subjects isolate your end users from each other, not from you. Your API key is an organization credential, so treat it like one.

`GET /v1/me` reports `subjectDefault`, which is always the key owner's user id: the identity used when no selector is sent. It is deliberately **not** the resolved `X-Exo-Subject` value, because internal subject user ids are an isolation detail and never leave the API. Do not use `subjectDefault` to confirm which subject a call read.

## When it goes wrong

| What you see                                       | Why                                                                        | What to do                                                                                                                                 |
| -------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `403 permission_denied` on a normal call           | The header names a subject that was never provisioned                      | `PUT` the subject first. An unprovisioned selector is refused before the handler runs. See [permission\_denied](/errors/permission_denied) |
| `404 subject_not_found` on `GET /v1/subjects/{id}` | No such subject in this organization                                       | Provision it, or check for a typo in the id                                                                                                |
| `409` on `PUT`                                     | The id collides with the key owner                                         | Choose a different id                                                                                                                      |
| `409 subject_mapping_invalid` on `DELETE`          | The mapping is corrupt and points at a non-service user                    | Nothing is erased. Contact support with the `requestId`. See [subject\_mapping\_invalid](/errors/subject_mapping_invalid)                  |
| `422` on `PUT`                                     | The id is longer than 128 chars or uses characters outside `A-Za-z0-9._:-` | Normalize ids before sending, especially if they came from an email                                                                        |
| Data lands on the wrong person                     | The selector was sent in the body instead of the header                    | Move it to `X-Exo-Subject`                                                                                                                 |

## Next

<Columns cols={2}>
  <Card title="Subjects and isolation" icon="shield" href="/concepts/isolation">
    The isolation model underneath, and why `subjectDefault` is never the resolved selector.
  </Card>

  <Card title="Forgetting and portability" icon="eraser" href="/guides/forgetting">
    Erase one subject completely, or export everything they ever stored.
  </Card>
</Columns>
