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

POST /v1/experience

Capture an experience envelope — the single write path for events.

POST
/v1/experience
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 as this server runs it (CORTEX_MINT_MAX_LIFETIME_PERSON, _AGENT and _SERVICE since 0.14.1; by default 7,776,000 s for a user:, 604,800 s for an 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

Query Parameters

consistency?string

consistency_tokens_v1: 1 or true, in any case (TRUE, True), adds a consistency_token to a successful receipt, naming the log offset that covers this write; 0 or false adds none; any other value is 400 INVALID_QUERY (details.field consistency), before the write runs. Ignored by a server that does not advertise the capability. The token is issued at the root of the scope ?path= or ?scope= names, else the body's scope (a bulk's: the root of the first item it stored, never of one it refused), on every 2xx JSON answer (207 included), after the body's own keys; it is the log's next offset when the receipt is sent. A token this node cannot issue leaves the committed receipt as it is, with no token. An empty value is false.

Value in

  • "1"
  • "true"
  • "0"
  • "false"
wait?string

When the route answers. Omitted or accepted: 202 at the durable capture point, indexing continuing in the background (an immediate recall can race it). captured: 200 {status: captured} at the same point. indexed: 200 once the indexer has indexed every log offset up to this write, whatever its scope, and the event reached indexed, within one 30 s budget for both waits. consolidated: as indexed and then, with enrichment on (CORTEX_ENRICHMENT_DELAY_SECONDS), until deferred enrichment applied this event (a triple or an extract: [] event does not wait), within the same budget. settled: cannot succeed in this release: the write is captured and indexed, then the route answers 503 SETTLEMENT_UNAVAILABLE. A write that references a blob waits for the indexer, within the same budget, whatever wait says. An empty value is accepted; any other value is 422 INVALID_BODY naming the valid values.

Value in

  • "accepted"
  • "captured"
  • "indexed"
  • "consolidated"
  • "settled"
scope?string

Read only with consistency=1, and only when path is absent: the scope path whose root the consistency_token names, in place of the body's scope. Its root must be one the body writes: a scope under another root is 403 POLICY_DENIED, details {reason: token_root_not_written, scope}, before the write runs, and nothing is written. Leave it out, or send the write's own scope.

path?string

As scope, and read before it: the consistency_token's root with consistency=1. Read by nothing else here.

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.

Envelope on the default router; NativeCaptureRequestV1 on the native host.

The experience envelope (API reference §3.5): the body of POST /v1/experience, each items[] entry of POST /v1/experience/bulk and /v1/experiences, and each canonical-envelope line of POST /v1/import and /v1/import/jsonl. Unknown keys at the top level and inside context, directives and refers_to entries are 422 INVALID_BODY (details.field_path, details.reason, details.schema). Write identity, which an idempotency_key replay is compared on, is the envelope after normalization with every string in Unicode NFC and source fragments sorted by fragment_id, without idempotency_key, both observed_at spellings, metadata, language, scripts, context.timezone and context.refers_to. The JSON Schema served at /v1/schemas/experience_envelope covers the fields that predate provenance and the observed_at and metadata aliases.

Response Body

application/json

application/json

application/json

application/json

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/experience" \  -H "Content-Type: application/json" \  -d '{    "scope": "string",    "modality": "string",    "content": {      "kind": "message",      "role": "user",      "text": "string"    }  }'
{  "event_id": "string",  "status": "captured",  "wal_offset": 0,  "stages_completed": [    "captured"  ],  "replayed_from_idempotency": true,  "warnings": [    "string"  ],  "redirected_to": "string",  "consistency_token": "string",  "consistency_reset": true,  "derived_through": 0,  "access_restored_at": "2019-08-24T14:15:22Z"}
POST
/v1/experience/bulk
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 as this server runs it (CORTEX_MINT_MAX_LIFETIME_PERSON, _AGENT and _SERVICE since 0.14.1; by default 7,776,000 s for a user:, 604,800 s for an 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

Query Parameters

consistency?string

consistency_tokens_v1: 1 or true, in any case (TRUE, True), adds a consistency_token to a successful receipt, naming the log offset that covers this write; 0 or false adds none; any other value is 400 INVALID_QUERY (details.field consistency), before the write runs. Ignored by a server that does not advertise the capability. The token is issued at the root of the scope ?path= or ?scope= names, else the body's scope (a bulk's: the root of the first item it stored, never of one it refused), on every 2xx JSON answer (207 included), after the body's own keys; it is the log's next offset when the receipt is sent. A token this node cannot issue leaves the committed receipt as it is, with no token. An empty value is false.

Value in

  • "1"
  • "true"
  • "0"
  • "false"
wait?string

When the route answers. Omitted or accepted: 202 once the indexer has indexed the batch, or once its budget, min(30 s + 1 s per newly stored item, 600 s), ran out, whichever comes first; never 408 (an item whose indexing failed within the wait is reported as a failure). captured: 202 at the durable capture point (raw-first; poll GET /v1/experience/status), unless an item references a blob, which waits as accepted does. indexed, consolidated: 200 with stage once every event reached the stage, within one budget of min(30 s + 1 s per item, 600 s) covering the indexer wait and the stage barrier, else 408 WAIT_TIMEOUT. settled: cannot succeed in this release: after the batch is indexed the route answers 503 SETTLEMENT_UNAVAILABLE; its budget is CORTEX_SETTLEMENT_TIMEOUT_MS (default 600000 ms, clamped to 1000-1800000). An empty value is accepted; any other value is 422 INVALID_BODY.

Value in

  • "accepted"
  • "captured"
  • "indexed"
  • "consolidated"
  • "settled"
scope?string

Read only with consistency=1, and only when path is absent: the scope path whose root the consistency_token names, in place of the body's scope. Its root must be one the body writes: a scope under another root is 403 POLICY_DENIED, details {reason: token_root_not_written, scope}, before the write runs, and nothing is written. Leave it out, or send the write's own scope.

path?string

As scope, and read before it: the consistency_token's root with consistency=1. Read by nothing else here.

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/experience/bulk and /v1/experiences. Send items or its alias experiences, not both. Unknown top-level keys are 422 INVALID_BODY.

Body of POST /v1/experience/bulk and /v1/experiences. Send items or its alias experiences, not both. Unknown top-level keys are 422 INVALID_BODY.

Response Body

application/json

application/json

application/json

application/json

application/json

application/json

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/experience/bulk" \  -H "Content-Type: application/json" \  -d '{}'
{  "batch_id": "string",  "accepted": 0,  "lifecycle_stream": "string",  "event_ids": [    "string"  ],  "stage": "indexed",  "results": [    {      "index": 0,      "event_id": "string",      "replayed_from_idempotency": true,      "warnings": [        "string"      ],      "redirected_to": "string"    }  ],  "failures": [    {      "index": 0,      "error_code": "string",      "error": "string",      "retriable": true,      "details": null,      "event_id": "string",      "remediation": "string"    }  ],  "partial": true,  "degraded": {    "embedding_provider_unhealthy": true,    "consecutive_failures": 0,    "last_error": "string",    "note": "string"  },  "consistency_token": "string",  "consistency_reset": true,  "derived_through": 0,  "access_restored_at": "2019-08-24T14:15:22Z"}
GET
/v1/experience/status
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 as this server runs it (CORTEX_MINT_MAX_LIFETIME_PERSON, _AGENT and _SERVICE since 0.14.1; by default 7,776,000 s for a user:, 604,800 s for an 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

Query Parameters

event_id?string

The event id a receipt returned (evt_...). A malformed one is 422 INVALID_BODY. Ignored when idempotency_key is sent.

idempotency_key?string

The write's key, as sent; it wins over event_id. One of the two is required (422 INVALID_BODY).

scope?string

The scope the write was sent to, or the scope it landed in: a write a run redirected to its writer's private space is found by either. With idempotency_key it is matched against every namespace of the caller's key (the namespace the write was keyed under, and its row's scope); without it every namespace of the caller is searched. For an event_id, the event's own scope or the scope its claim was keyed under must equal it. A malformed path is 400 INVALID_QUERY. It is also the root at_least is demanded at.

at_least?string

consistency_tokens_v1: a consistency token from an earlier response, unchanged. The read first waits, before it runs, until this node's registry has reached the token's revision and its indexer has indexed below its offset, within CORTEX_CONSISTENCY_WAIT_MS (default 10000, at most 30000), at the root scope names (else path or root, else the token's own root); a token of another incarnation or epoch is ignored and the answer says consistency_reset: true. Ignored by a server that does not list the capability.

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

Response Body

application/json

application/json

application/json

application/json

application/json

curl -X GET "https://example.com/v1/experience/status"
{  "found": true,  "event_id": "string",  "status": "captured",  "stages_completed": [    "captured"  ],  "failure_reason": "string",  "resend": "no",  "note": "string",  "consistency_token": "string",  "consistency_reset": true,  "derived_through": 0,  "access_restored_at": "2019-08-24T14:15:22Z"}

Required fields

Only scope, modality, content are required

The three required fields are scope, modality, content. context, context.observed_at, and idempotency_key are optional — the server returns 202 without them (omitting any of the three required fields → 422).

Wait semantics

Omitted ?wait= returns captured, not fully indexed

With ?wait= omitted the write returns status: captured (202) — the event is in the WAL and read-your-writes holds on recall, but it is not yet indexed. Pass ?wait=indexed to block until BM25 + HNSW have the event, ?wait=consolidated to block on consolidation. ?wait=accepted is an explicit alias for the omitted default, and ?wait=captured blocks only until the WAL append; any other value 422s listing the valid set (accepted, captured, indexed, consolidated, settled). Every response includes replayed_from_idempotency; the lifecycle_stream URL comes only with the async 202 response, not with a waited 200.

Use wait=indexed on a single-node server

wait=settled needs a coordinator process with artifact workers; the default single-node server answers it with 503 SETTLEMENT_UNAVAILABLE. Use wait=indexed. The same release added a second request body shape on this route, NativeCaptureRequestV1, plus POST /v1/experience/resume and a POST form of /v1/experience/status; all three belong to the native memory host profile. On the default router POST /v1/experience/resume is 404 and POST /v1/experience/status is 405 (the GET form above is served) — see Native memory host.

Wait deadline and 408 WAIT_TIMEOUT

A waiting capture has one budget. When it runs out the server answers 408 WAIT_TIMEOUT (retriable: true). The event is already captured and durable, and indexing carries on in the background.

  • Single capture (?wait=indexed or consolidated): about 30 s, shared by the wait for the indexer to reach the event and the wait for the requested stage after it. details says what it waited for:

    {
      "error_code": "WAIT_TIMEOUT",
      "retriable": true,
      "details": {
        "event_id": "evt_01a0e9fbdebf7d50bbacdfa90dcab9fc",
        "status_url": "/v1/experience/status?event_id=evt_01a0e9fbdebf7d50bbacdfa90dcab9fc",
        "target_stage": "indexed",
        "waited_ms": 30004,
        "waiting_on": "indexer"
      }
    }

    waiting_on is indexer (the indexer had not reached the event) or stage (indexed, but not yet at the requested stage).

  • Bulk (/v1/experience/bulk?wait=indexed|consolidated): min(30 s + 1 s per item, 600 s), and it covers the batch's own inline indexing as well. A two-item batch answers after 32 s with details { batch_id, target_stage, waited_ms, waiting_on: "indexing", items, event_ids }; waiting_on: "stage" carries accepted instead of items. event_ids lists the batch's events in item order, the same list a 200 returns.

  • After a 408, poll status_url, or GET /v1/experience/status?event_id= for each id in event_ids. Don't re-send blindly: a re-send replays (never duplicates) only a capture or bulk item that carried its own idempotency_key. One sent without a key gets a key the server mints and never returns, so re-sending it writes it again. Send an idempotency_key on every capture that waits, bulk items included.

No status is not proof the write was lost

A status lookup that has no record of a write (for example found: false after a timeout and a restart) means the server no longer holds its receipt, not that the event is missing: the write can be stored and recallable all the same. Before re-sending, look for the event itself:

  1. GET /v1/events/{event_id}?scope=<its scope> with the event_id the write returned (see GET /v1/events), or, if you never got an id, recall something only that write contains.
  2. Re-send only when the event isn't there. A re-send with the write's own idempotency_key replays instead of writing twice; one without a key writes again.

The status answer says it outright in resend, and a lookup by event_id reads the stored write, so it answers the same after a restart:

resendWhenWhat to do
nothe write is storedNever re-send.
new_keystatus: "failed"Re-send the data under a new idempotency key.
if_sent_within_24hfound: false by idempotency_keyReceipts last 24 hours: re-sending is safe for a write sent within them; for an older one, look the write up first.
check_firstfound: false by event_idNo stored write you can see under that id (erased, forgotten or not visible). Don't re-send on this answer.
  • A bulk sent with no wait has no deadline, as before: it still indexes its batch before it answers 202, so a slow embedding provider delays that answer by as long as its calls take.

Source descriptor

An optional source object (schema version 2) records the authoritative source and exact fragments the envelope represents: source_id, content_kind, a sha256:/blake3: fingerprint of the bytes, and per-fragment locators (document page, conversation turn, code file and lines, and so on). It is stored verbatim and echoed on reads. source.modality must equal the envelope modality or the write is 422 INVALID_ENVELOPE; unknown locator fields are 422 INVALID_BODY. Field reference and example: Experience Envelope.

Content kinds

content.kind is one of message, text, json, blob_ref, triple. A triple is nested under content.triple (a flat subject/predicate/object directly in content → 422 "missing field triple"), and its object is a full struct — type + datatype + value:

{
  "kind": "triple",
  "triple": {
    "subject": { "type": "entity", "id": "ent_acme" },
    "predicate": "seats",
    "object": { "type": "literal", "datatype": "string", "value": "200" }
  }
}

Omitting datatype → 422 "missing field datatype".

A triple capture writes its fact directly, with no enrichment and no LLM call: the extraction model never sees it, whatever directives.extract says. With ?wait=indexed the fact is on GET /v1/facts as soon as the write returns, with or without enrichment.

Language, time zone and referenced times

Three optional hints, each gated on a capability in GET /v1/admin/version capabilities[]:

  • language (BCP-47, e.g. hi-Latn) and scripts[] (ISO 15924, at most 8), capability capture_language_v1. Values are canonicalized (HI-latn is stored as hi-Latn, latn as Latn). A malformed tag or more than 8 scripts → 422 INVALID_ENVELOPE.
  • Every event gets one language tag. Your language wins, then a lang:<tag> label in context.labels, then detection: Devanagari text is stored as und-Deva, and Latin-script Hinglish as hi-Latn. Event reads and recall items return language, scripts and language_source (hint, source, label or detected). An event that is English by default stores no tag, so it reads back exactly as before; an explicit "language": "en" is stored.
  • context.timezone: an exact IANA zone name. Asia/Kolkata and the older alias Asia/Calcutta work; IST, +05:30 and Asia/Mumbai → 422 INVALID_ENVELOPE.
  • context.refers_to[] (capability refers_to_v1): the dates or times the memory talks about, as local dates (YYYY-MM-DD) or date-times without an offset, in from, to or candidates[], with an optional zone and precision. Recall can boost them with temporal.refers_during. A phrase such as "next friday" → 422 INVALID_ENVELOPE.

None of these is part of the write identity. A retry with the same idempotency_key and body but different hints replays the first write (the first write's hints win) and adds warnings: ["conflicting_hint_on_replay: …"] to both the 202 and a waited 200.

Gate new fields on capabilities, not on the version

A server that does not list a capability refuses its fields with 422 INVALID_BODY (as unknown fields). Check capabilities[] before sending language, scripts, context.timezone or context.refers_to.

Text blobs

A blob_ref capture of a text blob decodes the bytes by byte-order mark, a declared charset=, or strict UTF-8 (capability blob_text_decode_v1). Bytes that are none of these fail the write with 422 BLOB_TEXT_ENCODING_UNKNOWN (retriable: false; details { blob_id, content_type, reason, charset }, e.g. reason: "undeclared_non_utf8"). Declare the charset on upload (Content-Type: text/plain; charset=windows-1252) and the same bytes decode correctly. In /v1/experience/bulk one such blob fails the whole batch, and none of its items is stored. The event records the blob it came from in source_blob_ids; forgetting or erasing the event deletes the blob unless another live event still uses it (see Blobs).

Errors

Error codes are UPPERCASE

Error codes are uppercase: INVALID_BODY (e.g. a bad scope — must be type:name like org:acme), IDEMPOTENCY_CONFLICT (same key + different body — 409, retriable: no), POLICY_DENIED, RATE_LIMITED. The error body is flat: { "error_code", "message", "retriable", "request_id", "details": { "field_path", "reason", "schema" } }. details is present only when the error has any: INVALID_BODY carries it, INVALID_ENVELOPE does not.

A capture that would register a new scope inside a running forget or erasure waits for it (up to CORTEX_DESTRUCTIVE_LEASE_WAIT_SECS, default 120 s) and then gets 409 DESTRUCTIVE_OPERATION_IN_PROGRESS (retriable: true, Retry-After: 5); nothing was written, so re-sending is safe. Captures into existing scopes never wait.

Notes

  • Idempotency keys are scoped to the scope, actor and key, with /v1/experience and /v1/experience/bulk sharing one bucket (a bulk item replays a single write with the same key and body). The same key and body sent to another scope, or by another actor, writes a new event. Keys are at most 64 characters (a longer one → 422 INVALID_ENVELOPE) and expire after 24 hours; a repeat within the window with the same body replays (replayed_from_idempotency: true).
  • An unknown modality is stored verbatim (202) — the field is a free string, not an enum.
  • Bulk ingest: POST /v1/experience/bulk (in the spec above) and its alias POST /v1/experiences share one handler, with identical validation and response. The body takes items (or the alias experiences), an array of the same envelopes, plus an optional ordering string (strict_temporal | batch_throughput); a non-string value → 422. An empty array → 422; the only accepted top-level fields are experiences, items, and ordering (anything else → 422). The response is a batch envelope: { batch_id, accepted, lifecycle_stream, event_ids, results: [{ index, event_id, replayed_from_idempotency }] }; with ?wait=indexed it is a 200 that adds stage: "indexed", or a 408 WAIT_TIMEOUT listing the batch's event_ids.
  • Facts/Beliefs/Understanding extracted from text are derived asynchronously and require enrichment — on a content-only self-host they stay empty (triple captures, above, are the exception). See Self-hosting defaults.
  • directives.extract is honored on POST /v1/experience and /v1/experience/bulk: the stages are stored on the event and applied when its derived layers are built. extract is a list of stage names, not a flag: omitted or null runs every stage, extract: ["beliefs"] or ["understanding"] run enrichment, extract: [] turns it off for the event, and extract: false or "none" → 422 INVALID_BODY. directives.embed is validated but doesn't change how the event is embedded.

Governed writes

  • directives.extract limits learning to the stages it names. Each stage yields only its own outputs: entities gives entities and no facts; facts gives facts and no entities; beliefs gives facts too, since beliefs are built from them; understanding alone extracts nothing; [] learns nothing, and the event stays indexed and searchable. An event with a list joins episodes only when the list names episodes. An omitted list learns everything. For facts from an entities pass, name both: ["entities", "facts"].
  • A directive nothing reads says so. consolidate_into, confidence_floor, ttl_for_belief_layer, embed and async are accepted, but nothing acts on them: an ack that names one carries directive_not_enforced:<name> in its warnings.
  • memory:remember. Under a derivation policy in explicit mode, enrichment learns only from events labelled memory:remember or carrying a non-empty directives.extract.
  • derived_by (capability derived_by_v1): a typed triple written back by your own pipeline carries derived_by: {name, version}, served under include=derivation and reported by GET /v1/derivation/status?derived_by=. It is accepted on typed triples only (422 DERIVED_BY_INVALID elsewhere).
  • Writer rules. A policy may reserve a field for certain writers: a typed triple, a bulk item or an import row that a writer rule refuses is 409 WRITER_NOT_ALLOWED, and nothing is stored (in a bulk, only that item fails). See Writer rules.
  • Sessions. Label an event session:<id>, or give its source a session locator, when a read-time use rule may apply: the rule compares each event's session with the read's session.

On this page