Skip to main content
Exo is a record of how someone thinks, so the delete path matters as much as the write path. This page covers the four levels of erasure, what each one actually destroys, and how to take the data with you first.

Before you start

  • A key with the right scope. Erasure is deliberately narrow: memory:forget for a single node, subjects:provision for a subject, admin for a purge. See Scopes.
  • Certainty about the level. These verbs are not interchangeable, and three of the four cannot be undone.
  • An export, if the data has any chance of being wanted again. Take it before, not after.

Pick the right verb

Superseding is not forgetting, and it is usually what you want. If someone changed their mind, supersede the old belief: authority transfers to the successor, the old node stays queryable, and “what did they used to think?” still has an answer. Forgetting destroys that history. Reach for erasure when the requirement is privacy, not accuracy. See Belief and supersession.

Export first

The export is the portability guarantee and, in practice, the only backup you get. Erasure has no undo at the subject level and above.
1

Request the export

scope: "user" exports the effective subject. scope: "org" exports every member’s data and requires the admin scope, the same posture as org-wide usage reads.format is json. It is the only value the contract accepts.
2

Poll until it is ready

bytes, counts and downloadUrl are null until the export succeeds. Read counts before you erase anything, because it is the last honest statement of how much existed.
3

Download it

downloadUrl is an authenticated route, not a presigned blob link. It needs the same credentials as every other read, which means the document is never reachable by URL alone and a leaked link is not a leaked export. It also means you cannot hand the URL to a browser tab without the header.
Downloading an export that has not finished successfully returns 409 export_not_ready. Poll first.

Forget one memory

DELETE /v1/graph/nodes/{id} is the erasure verb for a single node.
Two things happen, and the difference between them is the whole design:
  • The content is erased. The node’s chunks, which is where the retrievable and searchable text lives, are deleted outright. Search, retrieve, recall, brain training and the dashboard all stop seeing it. That is what makes this a real erasure verb rather than a hidden flag.
  • The node row survives as a tombstone, for structure and audit, for 30 days. After the window it is physically purged.
Re-deleting an already-tombstoned node is a no-op success, so a retried delete is safe. A node you do not own is 404 node_not_found, never a 403, because a 403 would confirm the node exists.
graph:write is not enough to forget. Erasure needs memory:forget. A write key holds both, but a key minted with only graph:write can edit and supersede and cannot destroy content. Give integrations that key. See Scopes.

Undelete restores structure, not content

contentRestored is always false. Forget erased the text and no undelete can bring it back. What returns is the node’s graph position, title, metadata and authority. Edges removed around the node stay removed. The restored node is visible and linkable but contributes no retrievable text until you write new content to it with PATCH /v1/graph/nodes/{id}. Past 30 days you get the same 404 as a node that never existed, whether or not the collector has physically removed the row yet, because the tombstone is gone by contract at that point. Restoring a node that is not tombstoned is a no-op success with restored: false.

Erase one end user

DELETE /v1/subjects/{id} is the documented right-to-be-forgotten path for a single end user of your product.
204 with no body, and idempotent: a second call on an already-erased subject also returns 204. Requires subjects:provision, which an admin key also carries. What it removes:
  • Every row that subject owns in the tenant schema.
  • The subject mapping and its internal user.
  • The subject’s uploaded import files and finished export documents, deleted from storage after the rows commit. Erasure reaches blob storage, not only the database.
What it does not remove: the organization’s trained ExoBrain. The brain is trained on every subject, so deleting it for one erasure would destroy everyone else’s model. It is marked stale instead, and retrieval reports degradationReason: "brain_stale" until the organization retrains. See Training and serving an ExoBrain.
If the mapping’s internal user is not a service user, the call aborts with 409 subject_mapping_invalid before any row is erased. It fails closed. A corrupt mapping never results in a half-deleted subject.

The two-phase purge

POST /v1/purge is the admin ceremony for erasing at scale. It runs in two phases, and the dry run is the default, so a single mistyped call cannot destroy anything.
1

Dry run: count the impact

Nothing was deleted. Zero-row tables are omitted from counts so the payload stays readable, and totalRows is the sum of everything reported. Show these numbers to whoever asked for the deletion before you continue.
2

Confirm within five minutes

Phase two reports actual per-table deleted counts under the same counts keys, plus purgedAt.
The confirm token is bound to this organization, this scope, this subject, this admin and the impact the dry run reported. If new data has landed since the dry run, the confirm is refused with 409 purge_impact_changed and nothing is deleted. Re-run the dry run, review the new numbers, confirm with the fresh token. That is why purge is deliberately not replayable by Idempotency-Key. A replayed destructive call is a footgun, so every confirm must present a valid, unexpired, impact-bound token. See Idempotency.
scope: "org" empties the organization’s knowledge graph and keeps the organization itself, its users and its API keys. It also deletes uploaded imports, finished export documents and the trained ExoBrain, since the brain is derived from the graph being emptied. Retrieval falls back to the live rows until you retrain.subjectId is required for scope: "subject" and rejected for scope: "org".To delete the account itself rather than empty it, use DELETE /v1/me.
skipped lists tables the purge could not clear: absent from an older schema, or blocked by a foreign key. It is present only when non-empty, precisely so a purge that left rows behind can never look identical to one that had none.If skipped is present, the erasure is incomplete. Do not report it as finished.

Delete the account

This tears down the whole account. Sending an X-Exo-Subject selector with it is a 400: deleting an account is not something you do on someone else’s behalf, so the selector is refused rather than ignored. Sandbox keys are refused too.

When it goes wrong

You are one scope short. Forgetting a node needs memory:forget, erasing a subject needs subjects:provision, purging needs admin. A write key covers memory:forget but not subjects:provision. See permission_denied and Scopes.
Data landed between your dry run and your confirm, so the token no longer matches reality and nothing was deleted. This is the guard working. Re-run the dry run, check the new counts, confirm with the new token. If it keeps happening, pause ingestion for that subject first.
You called the download route before the export finished. Poll GET /v1/exports/{id} until status is succeeded, then follow downloadUrl. See export_not_ready.
Expected. Erasing a subject marks the organization’s brain stale, so degradationReason becomes brain_stale until you retrain. Results are still correct, they are served from a model that no longer matches the graph. See Brain path and hybrid path.
There is no route that does this. Forget erases content by design, and undelete explicitly reports contentRestored: false. If you have an export from before the deletion, re-ingest the content from it. If you do not, the text is gone.

Next

Subjects and isolation

Why per-subject erasure is clean: the partition it is erasing is physical.

Belief and supersession

The verb to use when the belief changed rather than the person leaving.