Skip to main content
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

A key that can provision

PUT and DELETE on subjects need subjects:provision. An admin key also qualifies.

A stable id per user

1 to 128 characters of A-Z a-z 0-9 . _ : -. Your own user id works well.

Somewhere to store it

Your database maps your user to their subject id. Exo never returns your mapping.

The shortest call that works

Provisioning takes no request body. The id in the path is the whole request.
201 Created
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.
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.
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

Provision on first sight

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.

Write and read as them

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.

List and inspect

GET /v1/subjects is keyset-paginated: page with nextCursor until hasMore is false. pageSize is coerced into 1 to 100 and defaults to 25.
200 OK
GET /v1/subjects/{id} fetches one, or returns 404 subject_not_found. Use it as an existence check.

Erase when they leave

DELETE /v1/subjects/{id} is the documented right-to-be-forgotten path. It returns 204 and is idempotent.
See Forgetting and portability for exactly what it removes.

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

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

Next

Subjects and isolation

The isolation model underneath, and why subjectDefault is never the resolved selector.

Forgetting and portability

Erase one subject completely, or export everything they ever stored.