POST /v1/experience
Capture an experience envelope — the single write path for events.
Authorization
bearer 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_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"
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"
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.
As scope, and read before it: the consistency_token's root with consistency=1. Read by nothing else here.
Header Parameters
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.
length <= 512subject_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.
length <= 8192Request 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"}Authorization
bearer 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_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"
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"
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.
As scope, and read before it: the consistency_token's root with consistency=1. Read by nothing else here.
Header Parameters
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.
length <= 512subject_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.
length <= 8192Request 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"}Authorization
bearer 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
The event id a receipt returned (evt_...). A malformed one is 422 INVALID_BODY. Ignored when idempotency_key is sent.
The write's key, as sent; it wins over event_id. One of the two is required (422 INVALID_BODY).
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.
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
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.
length <= 512subject_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.
length <= 8192Response 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=indexedorconsolidated): about 30 s, shared by the wait for the indexer to reach the event and the wait for the requested stage after it.detailssays 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_onisindexer(the indexer had not reached the event) orstage(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 withdetails { batch_id, target_stage, waited_ms, waiting_on: "indexing", items, event_ids };waiting_on: "stage"carriesacceptedinstead ofitems.event_idslists the batch's events in item order, the same list a200returns. -
After a 408, poll
status_url, orGET /v1/experience/status?event_id=for each id inevent_ids. Don't re-send blindly: a re-send replays (never duplicates) only a capture or bulk item that carried its ownidempotency_key. One sent without a key gets a key the server mints and never returns, so re-sending it writes it again. Send anidempotency_keyon 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:
GET /v1/events/{event_id}?scope=<its scope>with theevent_idthe write returned (see GET /v1/events), or, if you never got an id, recall something only that write contains.- Re-send only when the event isn't there. A re-send with the write's own
idempotency_keyreplays 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:
resend | When | What to do |
|---|---|---|
no | the write is stored | Never re-send. |
new_key | status: "failed" | Re-send the data under a new idempotency key. |
if_sent_within_24h | found: false by idempotency_key | Receipts last 24 hours: re-sending is safe for a write sent within them; for an older one, look the write up first. |
check_first | found: false by event_id | No 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
waithas no deadline, as before: it still indexes its batch before it answers202, 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) andscripts[](ISO 15924, at most 8), capabilitycapture_language_v1. Values are canonicalized (HI-latnis stored ashi-Latn,latnasLatn). A malformed tag or more than 8 scripts →422 INVALID_ENVELOPE.- Every event gets one language tag. Your
languagewins, then alang:<tag>label incontext.labels, then detection: Devanagari text is stored asund-Deva, and Latin-script Hinglish ashi-Latn. Event reads and recall items returnlanguage,scriptsandlanguage_source(hint,source,labelordetected). 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/Kolkataand the older aliasAsia/Calcuttawork;IST,+05:30andAsia/Mumbai→422 INVALID_ENVELOPE.context.refers_to[](capabilityrefers_to_v1): the dates or times the memory talks about, as local dates (YYYY-MM-DD) or date-times without an offset, infrom,toorcandidates[], with an optionalzoneandprecision. Recall can boost them withtemporal.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/experienceand/v1/experience/bulksharing 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
modalityis stored verbatim (202) — the field is a free string, not an enum. - Bulk ingest:
POST /v1/experience/bulk(in the spec above) and its aliasPOST /v1/experiencesshare one handler, with identical validation and response. The body takesitems(or the aliasexperiences), an array of the same envelopes, plus an optionalorderingstring (strict_temporal|batch_throughput); a non-string value →422. An empty array →422; the only accepted top-level fields areexperiences,items, andordering(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=indexedit is a200that addsstage: "indexed", or a408 WAIT_TIMEOUTlisting the batch'sevent_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.extractis honored onPOST /v1/experienceand/v1/experience/bulk: the stages are stored on the event and applied when its derived layers are built.extractis a list of stage names, not a flag: omitted ornullruns every stage,extract: ["beliefs"]or["understanding"]run enrichment,extract: []turns it off for the event, andextract: falseor"none"→422 INVALID_BODY.directives.embedis validated but doesn't change how the event is embedded.
Governed writes
directives.extractlimits learning to the stages it names. Each stage yields only its own outputs:entitiesgives entities and no facts;factsgives facts and no entities;beliefsgives facts too, since beliefs are built from them;understandingalone extracts nothing;[]learns nothing, and the event stays indexed and searchable. An event with a list joins episodes only when the list namesepisodes. 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,embedandasyncare accepted, but nothing acts on them: an ack that names one carriesdirective_not_enforced:<name>in itswarnings. memory:remember. Under a derivation policy inexplicitmode, enrichment learns only from events labelledmemory:rememberor carrying a non-emptydirectives.extract.derived_by(capabilityderived_by_v1): a typed triple written back by your own pipeline carriesderived_by: {name, version}, served underinclude=derivationand reported byGET /v1/derivation/status?derived_by=. It is accepted on typed triples only (422 DERIVED_BY_INVALIDelsewhere).- 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'ssession.