CortexDB Docs
API Reference

POST /v1/forget

Selective deletion — remove derived layers or redact events, with an audit record.

POST
/v1/forget
AuthorizationBearer <token>

PASETO v4 public token (or deployment gate key). Auth-disabled dev instances accept any caller.

In: header

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

application/json

application/json

application/json

application/json

curl -X POST "https://example.com/v1/forget" \  -H "Content-Type: application/json" \  -d '{    "scope": "string"  }'
Empty
POST
/v1/forget/preview
AuthorizationBearer <token>

PASETO v4 public token (or deployment gate key). Auth-disabled dev instances accept any caller.

In: header

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

application/json

application/json

application/json

application/json

curl -X POST "https://example.com/v1/forget/preview" \  -H "Content-Type: application/json" \  -d '{    "scope": "string"  }'
{  "preview_id": "string",  "scope": "string",  "estimated_deleted": {    "events": 0,    "episodes": 0,    "facts": 0,    "beliefs": 0,    "understanding": 0,    "blobs": 0,    "retained_shared_blobs": 0  },  "requested": 0,  "matched": 0}

Accepted fields

Only scope is required; idempotency_key is not a forget field

The accepted body is exactly scope, layers, selector, cascade, confirm_all, audit_note, reason, from_preview_id.

  • Only scope is required. The others default: layers empty means every layer, selector empty means the whole scope (and then needs confirm_all: true), and cascade is derived_only. { "scope": …, "confirm_all": true } is a complete request.
  • idempotency_key is not a forget field → 422 with a plain-text body ("unknown field idempotency_key, expected one of …"). An unknown cascade value gets the same plain-text 422.
  • reason is another name for audit_note (the SDKs use reason). Send one of them: both together → 422 "duplicate field audit_note".
  • from_preview_id executes exactly the operation a POST /v1/forget/preview described. An id the server did not issue for this caller, scope, layers, selector, cascade and confirmation → 422 UNKNOWN_PREVIEW_ID. Preview ids live in the server's memory, so a restart invalidates them (same 422).

A forget of specific events:

POST /v1/forget
{
  "scope": "org:acme/user:alice",
  "layers": ["events"],
  "selector": { "memory_ids": ["evt_01HX..."] },
  "reason": "user request"
}

What gets deleted

The default cascade keeps your memories

A forget that names no cascade runs derived_only, on every deployment, the hosted cloud included (its cloud_shared_saas preset sets forget.cascade.default: derived_only). derived_only removes derived data only (facts, beliefs, episodes, understanding): the memories you wrote stay, and recall, answers and GET /v1/events keep returning them. A whole-scope forget (confirm_all: true, no selector) under the default therefore deletes no memory; its warnings say so.

To delete memories:

  • Named memories: put their event ids in selector.memory_ids, with layers empty or including events. They are deleted under either cascade, the default included.
  • Every memory in a scope: POST /v1/erasures, or "cascade": "redact_events" with "confirm_all": true, which blanks every event in the scope.

Retry keys. A memory_ids forget and an erasure free the deleted memories' idempotency_keys: re-sending a deleted write with its old key stores it again. A scope-wide redact_events forget does not. Its blanked events keep their keys (a key lasts up to 24 hours from its write), so a re-send with an old key answers with the blanked event (replayed_from_idempotency: true) and stores nothing. Send new keys to write that content again.

  • A selector deletes what it names. selector.memory_ids naming events, with layers empty or including events, deletes those events under either cascade value: they disappear from GET /v1/events, GET /v1/events/{id} answers 404, and recall no longer returns them.
  • With layers that leave events out (for example ["facts"]), the event ids select only their derived records; the events stay and a warning says so.
  • cascade matters for the scope-wide shape (empty selector + confirm_all: true):
    • derived_only (default) removes the derived layers and keeps every raw event; the response warns that no raw event was deleted.
    • redact_events blanks every event in the scope; they no longer appear on GET /v1/events or in recall. redact_events with a selector deletes only the named events and warns that the cascade is scope-wide.
  • One cutoff. A scope-wide forget takes one server cutoff before it reads anything. Captures the server stamped before it are removed; captures stamped after it are never removed. A capture sent after the forget returns is stored and recalled normally.
  • Blobs. An event made from an uploaded text blob (content.kind: "blob_ref") records the blob in source_blob_ids. Forgetting the event deletes the blob unless another live event still uses it: deleted.blobs: 1 and GET /v1/blobs/{id} → 404, or deleted.retained_shared_blobs: 1 and the blob stays. Capability erasure_blob_cascade_v1.
  • Entity ids. about_entity, about_subject, or an ent_… id in memory_ids (under derived_only) delete the facts whose subject that id names, in the scope and its descendants, plus the beliefs and understanding they support. These selectors act on facts, which come from enrichment or triple captures.

Guards and errors

  • An empty selector requires confirm_all: true, else 422 EMPTY_SELECTOR_WITHOUT_CONFIRMATION.

  • A selector together with confirm_all: true is refused as ambiguous → 400 AMBIGUOUS_SELECTOR_CONFIRM_ALL ("confirm_all authorizes a scope-wide operation and would ignore the selector"). Send confirm_all: false (or omit it) to delete exactly the selection, or an empty selector with confirm_all: true for a scope-wide forget.

  • A storage backend failing mid-deletion → 502 FORGET_BACKEND_FAILED (retriable: true); nothing is reported as deleted. The same code on /v1/forget/preview means a derived store could not answer the dry run.

  • Retriable refusals. Nothing was changed in any of these; retry after Retry-After:

    StatusCodeWhen
    409AUTHORIZATION_STATE_CHANGEDScopes were registered or removed inside the forget's footprint (the scope, and its subtree for a whole-scope forget) on each of 5 attempts to authorize it (Retry-After: 1, details.retry_after_seconds: 1)
    409REDERIVE_IN_PROGRESSA derived-data repair job holds the scope (Retry-After: 30)
    503SERVER_SHUTTING_DOWNA graceful stop has begun (Retry-After: 5)

    Scope registrations elsewhere don't matter. A forget and its preview check the caller's authorization, then lock their footprint. A scope registered outside the footprint — any capture, from any tenant, that creates a brand-new scope — does not affect them. A scope registered or removed inside the footprint makes the forget re-check and try again, up to 5 times, before the 409 above. While new child scopes keep appearing inside the forgotten scope (dozens a second), many attempts end in that 409: pause those captures, or retry after them.

    While a forget runs it holds a lease on its scope footprint. Captures into existing scopes and reads never wait, but a capture that would register a new scope inside the footprint waits up to CORTEX_DESTRUCTIVE_LEASE_WAIT_SECS (default 120) and then gets 409 DESTRUCTIVE_OPERATION_IN_PROGRESS (Retry-After: 5), and /v1/scopes register, member edits and delete inside it get 409 SCOPE_STATE_CHANGED. Forgets usually finish in well under a second, so these are rare.

  • A forget runs detached from the HTTP request: if the client disconnects it still completes and still writes its audit row. Retrying the identical request is safe.

  • Request ids. Send an X-Request-Id header: the response echoes it, and the forget's gdpr: true audit row and the request's log line carry the same id, so the three can be joined.

Send one kind of selector per request

A request that combines memory_ids with about_subject is accepted (200) and deletes the named events. Keep each request to one kind of selector so its effect is clear. Error codes are uppercase (POLICY_DENIED, FORGET_BACKEND_FAILED).

Response

The response is { deleted, requested?, matched?, audit_id, warnings?, purge }:

  • deleted: per-layer counts (events, episodes, facts, beliefs, understanding), plus blobs / retained_shared_blobs only when they are non-zero.
  • requested / matched: present for a memory_ids selector: how many ids you sent and how many records the selector hit.
  • purge: the storage engine's own counts for this request: events, documents, edges, blobs, retained_shared_blobs, twins_redacted, bitemporal_records_erased, wal_rows_redacted, artifacts_cascaded. Every count is something this request changed, so repeating a forget reports matched: 0 and all zeros, and a scope-wide redact_events forget counts each event once.
  • warnings: present only when there is something to say: the selector matched nothing, layers did not name events, redact_events was sent with a selector, or a scope-wide derived_only forget kept the raw events (pointing to redact_events or /v1/erasures).

On a scope-wide forget of an enrichment-on scope, purge.events also counts derived content rows (a fact, gloss or question-key row stored under the scope); use deleted.events for the event count.

{
  "deleted": { "events": 1, "episodes": 0, "facts": 0, "beliefs": 0, "understanding": 0, "blobs": 1 },
  "requested": 1,
  "matched": 1,
  "audit_id": "audit_01HX...",
  "purge": { "events": 1, "documents": 1, "edges": 0, "blobs": 1, "retained_shared_blobs": 0,
             "twins_redacted": 1, "bitemporal_records_erased": 0, "wal_rows_redacted": 0,
             "artifacts_cascaded": 0 }
}

Ids outside the scope. An id stored in another scope and an id that does not exist get the same answer: 200, every count 0, matched: 0 and the "selector matched no records" warning. The event in the other scope is untouched.

Preview

POST /v1/forget/preview takes the same body and returns { preview_id, scope, estimated_deleted, requested?, matched? } without changing anything. requested / matched appear only for an event-id selector. It counts only events not yet blanked, so a preview after a scope-wide redact_events forget reports 0. Pass preview_id as from_preview_id to execute exactly that operation; any change to the body → 422 UNKNOWN_PREVIEW_ID.

Forget vs erasures

/v1/forget is for operational deletion (derived layers, event redaction). For GDPR reference-counted WAL deletion with a preview → execute → status lifecycle, use /v1/erasures. The SDKs rename forget's fields — see the Python and TypeScript SDK pages.

On this page