CortexDB Docs
Docs for CortexDB 0.14.0, the version running on CortexDB Cloud (api.cortexdb.ai).
API Reference

POST /v1/recall

Read a stratified pack — layers, context block, provenance, and diagnostics.

POST
/v1/recall
AuthorizationBearer <token>

Authorization: Bearer . Four credentials authenticate: (1) the operator key, the static CORTEX_API_KEY: the trusted operator, acting as the principal X-Cortex-Actor names (default user:local), in billing tenant default; (2) a PASETO v4 public token this server's minter signed (POST /v1/auth/tokens, POST /v1/auth/signup, the mint_offline tool), of audience CORTEX_V1_AUDIENCE or, with CORTEX_V1_TENANT_AUDIENCES=1, a billing tenant's own cortexdb:tenant:; (3) since 0.14.0 (principal_chain_v1), a JWT (RS256 or ES256) or PASETO v4 public token of an issuers-file entry (CORTEX_AUTH_ISSUERS_FILE; [auth] issuers_file on a cluster data node), whose subject, audience and token id (:) the entry maps; (4) a commitment transport token, on /v1/memory/commitments/* only. HS256, none and every symmetric JWT algorithm are refused. With auth disabled (no operator key: CORTEX_API_KEY unset on a loopback bind, or CORTEX_INSECURE_NO_AUTH) no token is verified and every request is served as the trusted operator, as the X-Cortex-Actor it names or user:local. On the operator key, with auth disabled and in the unsigned development mode, an X-Cortex-Actor that is not a principal id is 400 INVALID_HEADER, details {part: header, field: X-Cortex-Actor, reason: the parse error} (since 0.14.0; before, 401 INVALID_BODY). X-Cortex-Actor must equal the token's sub; a token of an issuers-file entry in prefix, hash or directory mode and a call of /mcp may leave it out (when present it must match). Any other token sent without it is verified first: a bearer of no known token form (a mistyped operator key) is 401 MISSING_TOKEN, any other bad token gets its own refusal, as with the header, and only a token that verifies answers 401 ACTOR_MISMATCH, the header is required. Every authenticated response carries X-Cortex-Token-Expires-In and X-Cortex-Token-Expires-At (and a Warning header within 72 h of expiry); every error of the auth layer carries X-Cortex-Request-ID.

Authentication refusals, the same on every authenticated route, none retriable unless marked. A token that carries none of the principal-chain claims and was not signed by an issuers-file entry gets each code with no details, as in 0.13.x; an entry's token, and a token that carries a chain claim other than minter, gets details.issuer (the entry's id, minter, commitment or unknown) and the details named here. 401 MISSING_TOKEN: no Authorization: Bearer header, or a bearer that is neither the operator key nor a PASETO v4 public or JWT token (a wrong operator key), with or without X-Cortex-Actor. 401 INVALID_TOKEN: a PASETO token no registered key verifies, a token signed by a key other than the issuer its iss names included. 401 INVALID_TOKEN_SIGNATURE: a bad signature, an iss no issuer is registered under, a JWT algorithm other than RS256 or ES256, a malformed token, or, on a token without chain claims, an iat ahead of the clock; for an issuers-file entry's token details.reason issuer_disabled, token_type, critical_header, algorithm, kid_revoked or unknown_kid (none for a bad signature). 401 EXPIRED_TOKEN. 401 REVOKED_TOKEN: the token's id is revoked (an entry's token goes by :). 401 WRONG_TENANT: an aud the token's issuer may not name, or one that names no tenant. 401 INVALID_TOKEN_CLAIM (since 0.14.0), details.claim and details.reason: a principal-chain claim malformed (malformed, not_an_agent, same_as_sub, not_a_service, person_with_agent, run_start_without_run, after_iat, run_too_old, out_of_range; unknown_critical_claim, with details.critical); an issuers-file entry's token without exp, iat or jti (missing), with iat or nbf ahead of the clock (ahead), or living longer than the entry's max_lifetime_s (claim exp, lifetime_over_maximum, details.lifetime_s, details.ceiling_s); a token this server's minter signed whose exp - iat is above the ceiling for its subject, 604,800 s for a user: or agent:, 2,592,000 s for a service: or system: (claim exp, lifetime_over_ceiling, details.issuer minter, details.lifetime_s, details.ceiling_s; checked before the revocation, and counted instead while CORTEX_MINT_LIFETIME_ENFORCE=off); and, whatever issuer signed it, a token whose act names an AI employee of its tenant, the self of an employee record that is not delegable (claim act, act_not_delegable). 401 CLAIM_NOT_PERMITTED (since 0.14.0), details.claim: a chain claim the token's issuer is not bound to assert (an issuers-file entry's [issuer.claims]; minter from any issuer but this server's minter; the minter's own chain claims without the chain assertion), or an issuers-file entry's scopes entry outside its token's tenant root org: (claim scopes, details.entry). 401 SUBJECT_UNMAPPED (since 0.14.0), details.reason: an issuers-file entry's subject mapping produced no principal (missing, not_a_principal, kind, service, foreign_namespace, personal_data_in_id, malformed, too_long, not_in_directory, directory_not_here); and, on every route whatever issuer signed the credential, a principal whose entry in its tenant's directory (org:) is deactivated (deactivated; never the trusted operator). 410 TENANT_DELETED (since 0.14.0): the credential's tenant root, org:, is being deleted or was (POST /v1/erasures/tenant); never the trusted operator. 503 REVOCATION_UNAVAILABLE (retriable): the revocation list cannot answer, so every token is refused as an outage, never 401 REVOKED_TOKEN, and the list still fails closed: on a cluster data node while the node's revocation lease has expired (the metadata group has not confirmed its revocation set for 10 s, or the node has just started), and on a single node once the list's journal failed (a refused write latches the list). 503 ISSUER_KEYS_UNAVAILABLE (retriable): an issuers-file entry's JWKS cannot be fetched and no pinned or cached key verifies the token; Retry-After is the whole seconds until the key cache's next fetch (its backoff, 3 s doubling to 60 s; 3 s before the first fetch), at least 1, also in details.retry_after_seconds. 503 WAL_UNAVAILABLE (retriable): the directory did not read, so a directory-mode subject's principal, or a principal's deactivation, is unknown.

Authorization refusals any route gives where it checks a capability or a roster, none retriable. 403 POLICY_DENIED, details.capability and details.denied_by_tier (deployment, tenant, actor or scope): the deployment's or the tenant's policy denies the capability, the token's caps leave it out, a scopes entry names it but not at this scope, or the scope's roster does not admit the caller. Since 0.14.0 a token that carries scopes holds only the capabilities an entry names (: its subtree, ! that one scope, :* every scope and the checks made without one); any other is refused with details.link confinement, and counted instead while CORTEX_TOKEN_CONFINEMENT_ENFORCE=off. An agent chain token (one that carries act or run, or app or client resolving to agent) is confined so whatever the switch, and without scopes holds no capability. A tenant-audience token's :* entry at a scope outside its own org: root is details.link tenant. A token another principal minted (minter a principal other than its sub) also needs that minter to hold the capability under the deployment's and the tenant's policy and the role on the scope's roster: details.link minter, denied_by_tier scope, counted instead while CORTEX_MINTER_LINK_ENFORCE=off. A read whose credential stops being current while it runs (expired or revoked, or its issuers-file entry disabled or reloaded without its audience) is refused 403 POLICY_DENIED before it delivers. 403 APP_NOT_ADMITTED (since 0.14.0): in a tenant whose root sets apps_enforced, a chain whose application (app, or unlisted) is not admitted at the scope. 403 RUN_REQUIRED (since 0.14.0), details.scope: on the write routes (POST /v1/experience, each item of /v1/experience/bulk and /v1/experiences, each JSONL import line, POST /v1/beliefs), a token with act and no run.

The operator key under CORTEX_OPERATOR_DATA_READS (since 0.14.0, operator_break_glass_v1): allow, audit or deny; unset, deny on the cloud_shared_saas preset and allow on the other presets and with auth disabled; a tenant's own mode (PUT /v1/admin/access/tenant-settings) overrides it in that tenant. 403 BREAK_GLASS_REQUIRED, details {mode, class, route}: under deny, without a well-formed X-Cortex-Break-Glass, an operator-key action of class stored_rows (a route that returns stored rows, or a forget or erasure preview whose selector names about_subject, about_entity or predicate), admin_content (a content-returning admin route), private_region (a read whose scope or extra_scopes lie in a private region) or in_band (a mint of a token that carries a data capability, can mint, carries a registry-write capability or is for an Owner of a private region; a registration, an auto-provisioning first write included; a roster or tree edit that widens a private region or adds a service: principal; a write into a private region or a shared org-tree scope; a migration out of a private region; lowering a tenant's mode from deny; a deployment minter's mint of a token that carries a data capability). 400 INVALID_HEADER, in every mode: that header malformed, sent twice or longer than 512 bytes, details {part: header, field: X-Cortex-Break-Glass, reason: what is wrong with it}. Both refusals write the request's audit row as decision deny, each with its denial (check operator_access, path request#x-cortex-break-glass, decided_by_tier request; reason the 403's class, or break_glass_invalid for the 400, which names no scope), within the operator key's bound (its billing tenant and X-Cortex-Actor) (AuditRow). Its form: reason=incident|legal|support|security|migration|restore|other; ticket=1 to 64 characters of [A-Za-z0-9._:-]; optionally note=percent-encoded UTF-8 of at most 256 bytes; no other key, separated by semicolons. It is read only on the operator key's requests and on a deployment minter's POST /v1/auth/tokens, and ignored on every other credential; a cross-origin browser page cannot send it. 503 NOTICE_UNAVAILABLE: under deny, a header-bearing read of a private region whose notice to the region's person did not commit, so nothing was delivered: details.reason moving, limit, members_behind (Retry-After: 30) or refused (details.refusal), each retriable; arming_disabled and reserved_path_in_use, not retriable. A by-id read's notice is its row's region's, committed once the row is read (since 0.14.0; scope_unnamed is gone).

Before authentication, a build that enforces licenses answers every data route 402 LICENSE_EXPIRED, or 402 LICENSE_REQUIRED when it never held a license, not retriable; health, version, ready, /metrics, the published contract (the OpenAPI document, GET /v1/schemas and /v1/schemas/{name}), the console, every GET /v1/admin/* route, GET /v1/auth/whoami, POST /v1/export and, since 0.14.0, deleting data (POST /v1/forget and its preview, POST /v1/erasures and its preview, /v1/erasures/person, /v1/erasures/tenant, /v1/erasures/{id}/cancel, and GET /v1/erasures/{id}, /verify and /receipt) pass. Not an authentication refusal, and answered only where a write needs a newer shard apply format than the node's shard is armed for (change sets, application records, directory and SCIM writes, a person's erasure and a tenant's deletion, writes that carry atoms or provenance, rehomes and watermarks, a declared belief with provenance): 503 FORMAT_UPGRADE_PENDING, details {reason, format, node, paths, count} (reason, format and node alone on the directory, SCIM, rehome and watermark writes; reason alone on an application record or a reference root), retriable with Retry-After: 30 for members_behind only (arming_disabled and reserved_path_in_use wait for the operator). A plain node arms for format 10 at its first boot of 0.14.0 under the default ceiling, one way, and then never answers it.

In: header

Header Parameters

X-Cortex-Break-Glass?string

operator_break_glass_v1 (API reference §8.15.3): read only on a request the operator key (CORTEX_API_KEY) authenticates, and on a deployment minter's POST /v1/auth/tokens; ignored on every other credential. reason=<incident|legal|support|security|migration|restore|other>; ticket=<1 to 64 of [A-Za-z0-9._:-]>[; note=<percent-encoded UTF-8, at most 256 bytes>], exactly one header of at most 512 bytes; any other form is 400 INVALID_HEADER in every mode (details {part: header, field: X-Cortex-Break-Glass, reason}), and the request's audit row a deny. Under CORTEX_OPERATOR_DATA_READS=deny (the deployment's, or the tenant's own) an operator-key request that reads stored rows, a content-returning admin route, a private region's read, or an in-band path needs it, else 403 BREAK_GLASS_REQUIRED (details {mode, class, route}). A well-formed header is written to the audit row as {reason, ticket}, never the note.

Lengthlength <= 512
X-Cortex-Subject-Token?string

subject_token_shadow_v1: on a request the operator key authenticates, the token of the principal the operator acts for, verified by the same verifier (without waiting on the network and without the X-Cortex-Actor step) and evaluated in shadow by the access report (GET /v1/admin/access/report). It never changes the response: a repeated, oversized (over 8,192 bytes) or invalid header, or the operator key itself, is recorded as the shadow's invalid_token and the request runs as sent.

Lengthlength <= 8192

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Body of POST /v1/recall and POST /v1/recall/stream. Strict: a field not listed here is 422 INVALID_BODY (unknown field), and so are the pagination keys limit, offset, page, cursor and page_size on /v1/recall (bound the pack with budgets; page raw records with GET /v1/events). consistency and purpose are read off the body before it is parsed. A field whose capability GET /v1/admin/version does not list is refused with 422 INVALID_BODY (details.unsupported_fields, details.required_capabilities), its key's presence counting (language: null, scripts: [], temporal.timezone: null); subtree is flag-gated instead (409 SUBTREE_RECALL_DISABLED).

Response Body

application/json

application/json

application/json

application/json

application/json

application/json

application/json

application/json

application/json

application/json

curl -X POST "https://example.com/v1/recall" \  -H "Content-Type: application/json" \  -d '{    "scope": "string"  }'
{  "pack_id": "string",  "scope": "string",  "view": "raw",  "context_block": "string",  "layers": {    "events": [      {        "matched_fragments": [          {            "chunk_id": "55e808e1-8ddc-49f1-92c9-6bbdcdff1c83",            "text_source": "event_text",            "byte_range": {              "start": 0,              "end": 1            },            "locator": {              "kind": "conversation",              "session_id": "string",              "turn_index": 0,              "speaker": "string",              "message_id": "string"            },            "language": "string",            "matched_via": "vector"          }        ],        "extract": [          "facts"        ]      }    ],    "facts": [],    "beliefs": [],    "episodes": [],    "understanding": [],    "code": [      {        "repo": "string",        "path": "string",        "citation": "string",        "snippet": "string",        "generation": 0,        "score": 0      }    ],    "rollups": [      {        "id": "string",        "home": "string",        "unit": "string",        "level": "team",        "kind": "totals",        "topic": "string",        "part": 0,        "parts": 0,        "summary_safe": true,        "built_at": "2019-08-24T14:15:22Z",        "window": {          "from": "2019-08-24T14:15:22Z",          "through": "2019-08-24T14:15:22Z"        },        "inputs": [          {            "scope": "string",            "read_at": "reader"          }        ],        "totals": {          "events": 0,          "intents": [            {              "key": "string",              "count": 0,              "latest_at": "2019-08-24T14:15:22Z"            }          ],          "label_families": [            {              "key": "string",              "count": 0,              "latest_at": "2019-08-24T14:15:22Z"            }          ],          "measures": [            {              "predicate": "string",              "count": 0,              "sum": 0,              "latest": {                "value": 0,                "at": "2019-08-24T14:15:22Z"              }            }          ],          "inputs": [            {              "scope": "string",              "events": 0            }          ],          "other": 0        },        "text": "string",        "positions": [          {            "text": "string",            "inputs": [              "string"            ]          }        ],        "citations": [          {            "kind": "event",            "id": "string",            "scope": "string"          }        ]      }    ]  },  "knowledge_subgraph": {},  "provenance": {},  "diagnostics": {    "time_ms": {      "property1": 0,      "property2": 0    },    "policy": {      "tier_attribution": "string",      "capability": "string"    },    "filters_applied": [      "string"    ],    "knapsack_evictions": 0,    "_partial": true,    "degraded_sources": [      "string"    ],    "items": [      {        "event_id": "string",        "final_rank": 0,        "fused_rank": 0,        "variant_fused_rank": 0,        "channels": {          "property1": {            "rank": 0,            "score": 0          },          "property2": {            "rank": 0,            "score": 0          }        },        "variants": [          {            "index": 0,            "vector_rank": 0,            "bm25_rank": 0,            "fused_rank": 0          }        ],        "matched_via": [          "string"        ],        "reorder": {          "property1": {            "from": 0,            "to": 0          },          "property2": {            "from": 0,            "to": 0          }        }      }    ],    "drops": [      {        "event_id": "string",        "reason": "time_range"      }    ],    "channel_lists": [      {        "channel": "vector",        "variant": 0,        "len": 0      }    ],    "items_truncated": 1,    "filtered_results_not_in_ranked_pool": 0,    "scopes_traversed": [      {}    ],    "code_stage": {      "property1": "string",      "property2": "string"    }  },  "warnings": [    "string"  ],  "subtree": {    "mode": "ranked",    "complete": false,    "subjects_authorized": 0,    "subjects_with_candidates": 0,    "subjects_in_pack": 0,    "events_in_pack": 0,    "vector_strategy": "exact_scan",    "subjects": [      {        "alias": "string",        "scope": "string",        "events": 0,        "best_rank": 0      }    ]  },  "coverage": {    "searched": {      "scopes": [        "string"      ],      "scopes_total": 0,      "partitions": 0,      "truncated": true    },    "authorized": {      "scopes": [        "string"      ],      "scopes_total": 0,      "truncated": true    },    "gaps": [      {        "scope": "string",        "reason": "bound",        "left_out": 0      }    ],    "gaps_truncated": true,    "gaps_total": 0,    "watermarks": [      {        "source": "string",        "at": "2019-08-24T14:15:22Z",        "through": "string"      }    ]  },  "consistency_token": "string",  "consistency_reset": true,  "derived_through": 0,  "access_restored_at": "2019-08-24T14:15:22Z"}

Views

viewtraversalnotes
rawsingle scopefull event payloads
granular (alias local)single scopeall layers, exact scope only
holistic (default)scope + ancestorsrequires scope.read.holistic
descendscope + descendantsrequires scope.read.descend
lineage—supported (was undocumented in v1)
structuredsingle scopefacts + beliefs only

local is accepted

view: "local" is accepted on recall and behaves as granular.

Behavior notes

Recall is not cached by idempotency_key

Recall always runs fresh: repeated calls return a new pack_id each time, whatever idempotency_key you send. Keep a pack_id yourself if you need to refer back to a pack.

  • Filters take exactly one field, metadata, with attributes observed_actor, caller, modality, labels, intent (ANDed; values ORed). Unknown attributes → 422. No confidence filter.
  • exclude_content: true nulls large content/summary strings.
  • budgets.per_layer_limits hard-caps each layer (events: 0 drops the layer).
  • diagnostics = none (default) / summary / full. Timings are reported only for summary and full: time_ms holds about 47 phase-named keys (coordinator_recall, wal_hydrate, recall_total, …); with none it is an empty object. With capability recall_diagnostics_v1, full also returns per-item ranks, items[] = { event_id, final_rank, fused_rank, channels: { bm25: {rank, score}, vector: {rank, score} }, matched_via }, and channel_lists[] = { channel, len }, plus knapsack_evictions.
  • provenance.trail entries are { phase, elapsed_ms } (+ a context_contributors list; see the context receipt).
  • A body field easy to miss: prefer_exhaustive (exhaustive candidate sweep). The order field has its own section below.
  • Streaming: POST /v1/recall/stream emits plan → layer(×N) → context_block → provenance → diagnostics → done (the done event carries pack_id).
  • pack_id can be reused by POST /v1/answer via use_pack_id (same-scope only; cross-scope → 404).

Context block, budgets and long events

context_block is the text an LLM would see. With capability budget_excerpt_v1, no entry takes it past budgets.max_tokens. Events are placed whole in rank order while they fit; an event that doesn't fit is skipped, and each skipped event then gets the room still free at its rank position — whole if it now fits, otherwise as an excerpt of its best-matching chunks. The default budget is 4,000 tokens, about 14,000 bytes of context (CORTEX_RECALL_DEFAULT_MAX_TOKENS changes it).

For example, a 15,637-byte document queried for its second section gives a 3,320-byte block at max_tokens: 1000, with the excerpt starting at the matched section, and a 13,992-byte block at the default budget. An excerpted event's layers.events item carries the same excerpt, marked as a partial view:

"content": {
  "kind": "text",
  "text": "[…]
# Section 2: VPN troubleshooting …
[…]",
  "_partial": true,
  "_partial_reason": "budget_excerpt",
  "_original_bytes": 15637,
  "_excerpt_ranges": [{ "start": 3013, "end": 6219 }, { "start": 15564, "end": 15636 }]
}

Treat content.text as the whole text only when _partial is absent; GET /v1/events/{id} always returns the whole event. Slices are joined with […] markers; _excerpt_ranges are byte ranges into the stored text. CORTEX_RECALL_BUDGET_EXCERPTS=0 (or off, false) turns excerpts off and restores the pre-0.10.1 render. The server then leaves budget_excerpt_v1 out of capabilities[] and logs RECALL_BUDGET_EXCERPTS_DISABLED at startup, so a client that gates on the name knows excerpts are off.

For every language, English included: an assistant turn's In reply to user: … echo is dropped when that user turn is rendered in the same block; an event too large for the remaining budget does not end the block; and the first-ranked event gets no exception: it is excerpted like any other when it does not fit.

Matched fragments

For a long (chunked) event, layers.events[].matched_fragments[] (capability matched_fragments_v1) says which chunks matched: { chunk_id, byte_range: {start, end}, matched_via: "bm25" | "vector", locator: { kind: "document", section_path: [title, heading] }, language, text_source }. Byte ranges index the stored text, not an excerpt. section_path holds the Markdown headings in force where the chunk starts: the first # heading acts as the document title and later headings follow it. GET /v1/admin/events/{id}/chunks shows how any event was chunked.

The context receipt: which events the block renders

The provenance entry { "phase": "context_contributors", "context_contributors": [{ "event_id", "rank" }] } names every event context_block renders, including one the cross-layer knapsack evicted from layers.events (that row carries evicted_from_layers: true and is the only place the response names it).

Converting receipt ids

context_contributors[].event_id is a hyphenated UUID (01a0e70c-81fd-7cc2-b687-4e42ed600f50), while layers.events[].id and the event routes use evt_01a0e70c81fd7cc2b6874e42ed600f50. To match an entry or fetch the event, strip the hyphens and prefix evt_.

Who said it: recalled event identity

Every layers.events item carries the identity its event was captured with: caller, observed_actor, subject, scope_key, scope and wal_offset are the values GET /v1/events/{id} returns, in every view. What that changes for a client:

  • A filters.metadata.observed_actor recall now keeps events captured on someone else's behalf that those paths used to drop.
  • scope_key is the billing partition the event was captured under (default for the static operator key), not its scope. Compare scope when you need the scope.
  • context_block is unchanged.

A capture cancelled with POST /v1/lifecycle/memory-event/{id}/cancel is left out of every view: granular, raw, holistic, descend, prefer_exhaustive and order: "recency".

Ordering: relevance vs recency

The order field switches how the events layer is selected and rendered:

orderSelectionRendering
relevance (default)ranked retrieval (BM25 + vector + RRF), scored against the queryranked order
recencythe scope's newest eligible events by observed_at — a timeline tailnewest-first

recency replaces ranked retrieval for the events layer with a timeline tail: the newest eligible events by observed_at, capped by budgets.per_layer_limits.events (else CORTEX_RECENCY_TAIL_EVENTS, default 100). Eligibility (scope/tenant, filters, temporal) is identical to ranked mode — only selection changes. It's built for "recent/latest X" questions, where relevance ranking surfaces the most-repeated items and can drop the newest ones. An unknown order value → 422.

Use the holistic view for recency

order: "recency" applies in the holistic view (the recall default): holistic + recency + { events: 3 } returns the three newest events, newest first. The granular and raw views keep relevance order (ascending observed_at for events, and a per_layer_limits.events cap keeps the oldest). Use the default holistic view for "latest / most recent" reads.

/v1/answer forwards order and auto-infers recency for recency-shaped questions — see POST /v1/answer. The tuning knobs (CORTEX_RECENCY_TAIL_EVENTS / _ORDER / _INFERENCE) live in Recall Tuning.

Current values and corrections: newest_of_kinds

Ranked recall returns every past statement that matches the query. For something with one current value — a preference, a setting, a status — that means a late correction comes back beside the statements it corrects, and they can outnumber it:

[2026-01-05] Nigel prefers morning meetings, ideally 9am.
[2026-09-20] Correction from Nigel: he now prefers afternoon meetings after 2pm.
[2026-03-01] Nigel said again he likes early morning meetings at 9am.

Give each such thing a stable kind in context.intent when you write it, corrections included, and ask recall for the newest of that kind (v0.12.4+, capability memory_kinds_v1):

# Every statement of the preference, the correction too, carries the same intent
curl -fsS -X POST "$CORTEX_URL/v1/experience?wait=indexed" -H "$AUTH" -H 'Content-Type: application/json' -d '{
  "scope": "org:acme/user:nigel", "modality": "observation",
  "content": { "kind": "text", "text": "Correction from Nigel: he now prefers afternoon meetings after 2pm." },
  "context": { "intent": "preference.meeting_time" }
}'

curl -fsS -X POST "$CORTEX_URL/v1/recall" -H "$AUTH" -H 'Content-Type: application/json' -d '{
  "scope": "org:acme/user:nigel",
  "query": "What meeting time does Nigel prefer?",
  "newest_of_kinds": ["preference.meeting_time", "preference.seat"]
}'

For each kind listed, the pack holds the newest eligible event of that kind, first, whether or not the query matched it, and none of the older ones: the example returns only the correction. Older events stay listable (GET /v1/events?intent=preference.meeting_time) and erasable. Blank and repeated entries are dropped; more than 16 kinds is 422 INVALID_BODY; it can't be combined with subtree. When the scope can't be read whole in one bounded listing, the newest of each kind among the recalled events is kept and warnings carries newest_of_kinds_partial.

Use one kind per thing that has a single current value (preference.meeting_time, not preference): a kind serves exactly one event. Leave history questions ("what did Nigel prefer before?") on ranked recall.

Reading several scopes: extra_scopes

extra_scopes (v0.12.4+, capability extra_scopes_v1) adds up to 8 more scopes to the same ranked pass as scope: for example an application's own scope plus a shared source:slack and source:jira. /v1/recall/stream, /v1/answer and /v1/compose take the same field for every recall they make.

curl -fsS -X POST "$CORTEX_URL/v1/recall" -H "$AUTH" -H 'Content-Type: application/json' -d '{
  "scope": "org:acme/app:support",
  "extra_scopes": ["org:acme/source:slack", "org:acme/source:jira"],
  "query": "What did we promise Globex about the export bug?"
}'
  • Each extra scope is read exactly (its own events, no ancestors or descendants) and authorized as a direct granular recall of it would be: one the caller can't read refuses the whole request with that recall's 403.
  • Each contributes its own vector and BM25 candidates, and all of them meet in one fusion and one rerank. Their facts, beliefs, episodes and concepts join too.
  • Events keep their real scope in layers.events[].scope, and context_block lines from an extra scope carry its last segment after the time bracket: [2026-06-15 08:30 UTC] [source:slack] ....
  • Every extra scope must lie in the tenant subtree of scope (its root by default; CORTEX_TENANT_DEPTH=2 makes it the first two segments). One outside it, or more than 8, answers 422 INVALID_EXTRA_SCOPES. Repeats and scope itself are dropped; it can't be combined with subtree. Answers that use it are not cached.

On CortexDB Cloud the tenant subtree is your account, so extra_scopes can name any of your tenants and none of another account's.

Self-hosted default retrieval

On a default self-hosted instance recall runs BM25 + vector + RRF fusion only. Graph traversal needs a knowledge graph (CORTEX_ENTITY_GRAPH=1 + enrichment); cross-encoder rerank needs a Cohere key. See Self-hosting defaults.

On this page