Skip to main content
If you are building a product on top of Exo, every one of your end users needs their own memory. Their content must never leak into someone else’s retrieval, and when one of them asks to be forgotten you need a single call that actually erases them. A subject is that unit. You name it, Exo provisions it, and from then on a header selects it.
201 application/json
That is the whole object. The internal user id behind it is deliberately not returned: it is an isolation detail, and exposing it would let one caller reason about another’s storage.

What it is

Organization, the account your API key belongs to (in practice, a physically separate database schema, not a filtered view of a shared table). User, a member of that organization (in practice, the human whose key it is, which is who a call acts as when no selector is sent). Subject, an end user of your product, whose memory lives inside your organization (in practice, an id you choose, like customer_6412, that becomes a separate memory partition). The distinction that matters: your org is the tenant boundary Exo enforces at the storage layer. Subjects are the boundary inside it, enforced on every read and write by owner filtering. Both are real. Neither is optional.

Three levels, and who sees what

Subjects are not weaker than users. They are the same mechanism: both resolve to an owner that every query filters on. The difference is who provisions them and who they represent.

Selecting a subject

The selector is a header, X-Exo-Subject, not a body field. Send it on any call that reads or writes content, and that call acts as the subject instead of the key owner.
Omit the header and the call acts as the key owner. A single-user integration never sends it and never thinks about subjects at all. Provision before you select. Naming a subject that was never provisioned is refused with 403 rather than silently created, so a typo in an id cannot quietly start a second empty memory.

subjectDefault is not the selector

GET /v1/me reports a subjectDefault:
200 application/json
subjectDefault is always the key owner’s user id: the identity a request acts as when no selector is sent. It is deliberately not the resolved value of an X-Exo-Subject header you sent on that same call. If you are trying to confirm which subject a request acted as, the answer is the header you sent, not this field.

Provisioning and erasing

Subject ids are 1 to 128 characters of A-Z a-z 0-9 . _ : -. Use your own stable identifier for the end user. Do not use anything you would mind seeing in a log. DELETE /v1/subjects/{subject_id} is the documented right-to-be-forgotten path. It erases the subject’s rows, their mapping, and their internal user, then deletes their uploaded imports and export documents from storage once the rows commit. The organization’s ExoBrain is marked stale rather than deleted, because it was trained across every subject; retrieval keeps working on the fallback path until you retrain. See ExoBrain. This is a different verb from forgetting one node. DELETE /v1/graph/nodes/{node_id} erases one memory with a 30-day structural undelete window. Subject deletion has no undelete. Read How a memory is made before choosing between them.

Scopes

Subject management is gated separately from ordinary reads and writes: A write key can ingest and retrieve as an already-provisioned subject but cannot create or erase one. That split lets your application servers carry a write key while only your control plane carries admin. See Scopes.
Three families do not take X-Exo-Subject.POST /v1/brain/query and POST /v1/brain/answer read the API key owner’s own content by design.The subjects family itself takes the subject in the path, because there the subject is the object of the call rather than the identity performing it.POST /v1/purge takes subjectId in the body for the same reason, which is why the field is not named subject.DELETE /v1/me rejects the selector outright with a 400: deleting your whole account while pretending to be one of your end users is never what you meant.
scope: "org" on POST /v1/retrieve and POST /v1/search widens the search to the whole organization. That is intended for internal team search over your own members’ content, not for a B2B2C deployment where subjects are your customers. If you provision one subject per end user, keep scope at its default of user, which filters to the effective subject.Authorization never follows the scope field. It stays on the API key’s principal.
No, and edges make this concrete. Asserting an edge requires the effective subject to own both endpoints, and a proposal is only visible when the subject owns both of its endpoint nodes. Cross-subject relations are not expressible.
It is refused before the handler runs, so the failure is the same on every route that accepts the selector: 403 permission_denied. Provisioning is idempotent, so the fix is to call PUT /v1/subjects/{id} and retry. Calling PUT on every session start is the simplest way to never see this.

Limits

  • There is no cross-subject query. Nothing joins two subjects’ content.
  • There is no route that returns the internal user id behind a subject.
  • Subject erasure is not reversible and has no undelete window.
  • The ExoBrain is per organization, not per subject.
  • Data never crosses organizations. There is no cross-org read on any route, including the recall views.

Next

One memory per end user

The working guide: provision, select, and wire it into your product.

Provision a subject

Request shape, status codes, and the scope it needs.