Python SDK
Reference for cortexdbai — V1Client, AsyncV1Client, and the typed exception hierarchy.
The official Python client for CortexDB v1. V1Client is the full surface (50 methods); the async
AsyncV1Client covers a subset of it — see Async variant before you build on it.
pip install cortexdbaiRequires Python 3.10+. PyPI package: cortexdbai. Python module:
cortexdb.v1. The SDK version is independent of the server version; see Versioning and SDK compatibility.
Construction
from cortexdb.v1 import V1Client
client = V1Client(
api_url="https://api-v1.cortexdb.ai",
actor="user:alice",
bearer="v4.public.eyJpc3MiOi...",
timeout=30.0,
)| Argument | Default | Notes |
|---|---|---|
api_url | http://localhost:3141 | v1 surface URL. Defaults to a local server; pass https://api-v1.cortexdb.ai for cloud. |
actor | "user:default" | Sent as X-Cortex-Actor; must match the token's sub. |
bearer | None | PASETO v4 public token — or, self-hosted, the server's CORTEX_API_KEY value. Optional only when the server runs with CORTEX_INSECURE_NO_AUTH=1. |
timeout | 60 s | Per-request timeout. |
Use it as a context manager so the underlying requests.Session is cleaned up:
with V1Client(api_url="...", actor="user:alice", bearer=tok) as client:
pack = client.recall(scope="org:acme/user:alice")Auth
client.whoami()
# → { caller, tenant_id, deployment_preset, effective_capabilities, token }
client.mint_token(
subject="user:alice",
ttl_seconds=3600,
scopes=["org:acme/user:alice"],
)
# → { "token": "v4.public...", "expires_at": "2026-05-15T11:42:00Z" }whoami key is effective_capabilities
The capability list is returned under effective_capabilities (snake_case). mint_token accepts
scopes (plural) and caps alongside the singular scope.
Write path
client.experience(
scope="org:acme/user:alice",
text="Acme upgraded to 200 seats.",
role="user",
observed_at="2026-05-15T10:42:00Z",
idempotency_key="alice-chat-001",
wait=None,
)experience() modality is optional
Pass the content with text=, json_data=, transcript=, blob_id=, or triple= (plus role=).
modality= (an open-set label) and source= are optional keywords: if
modality is omitted it is taken from source=, else defaults to conversation.
client.experience_bulk(
scope="org:acme/user:alice",
items=[
{"text": "Acme upgraded to 200 seats.", "role": "user",
"observed_at": "2026-05-15T10:42:00Z", "idempotency_key": "k1"},
{"text": "Renewal is due in March.", "role": "user",
"observed_at": "2026-05-15T10:43:00Z", "idempotency_key": "k2"},
],
)experience_bulk() takes no ordering= parameter
experience_bulk(scope, items, *, wait=None) is the full signature — there is no ordering=
keyword. Items are ingested in list order; encode temporal order in each item's observed_at.
Each item is a flat dict with exactly one of text / json_data / blob_id / triple, plus optional
role, observed_at, labels, idempotency_key, modality, source (not a wire envelope).
wait ∈ None | "accepted" | "captured" | "indexed" | "consolidated" ("accepted" is an
explicit alias for the None default). With wait omitted, a write returns
captured (202) as soon as the WAL append succeeds — it is not fully indexed yet; pass
wait="indexed" to block until BM25 + HNSW have the event. For full envelope shape (subjects,
directives, blob refs, triples) see Experience Envelope.
Read path
client.recall(
scope="org:acme/user:alice",
view="holistic",
query="What did we decide about Acme?",
include=["events", "beliefs", "facts", "episodes"],
budgets={"max_tokens": 4000},
temporal={"natural": "last 30 days"},
)
# → StratifiedPack: layers, context_block, provenance, diagnostics, pack_id
client.answer(
scope="org:acme/user:alice",
question="Did Acme renew?",
view="holistic",
temporal={"natural": "last 30 days"},
)
# → { answer, citations, provenance, diagnostics, pack_id, as_of }answer_model is a cloud default
claude-opus-4-6 is the cloud default model. Self-hosted, /v1/answer routes through whatever
you configure in the answer lane (CORTEX_ANSWER_*). An answer_model= argument is only echoed in
diagnostics.answer_model; the configured model answers (details). /v1/answer is disabled on a self-hosted
instance until that lane is set. See
Self-hosting defaults.
Layer reads
client.events(scope="org:acme/user:alice", view="local", limit=50)
client.episodes(scope="org:acme/user:alice", view="local")
client.facts(scope="org:acme/user:alice", subject="ent_acme_corp", predicate="deal_stage")
client.beliefs(scope="org:acme/user:alice", view="local")
client.belief_why(belief_id="belief_01HX...")
client.understanding(scope="org:acme/user:alice", view="local")facts() wraps subject/predicate only — no as_of=
facts(scope, *, view='local', subject=None, predicate=None) is the full signature. There is no
as_of= keyword — passing one raises TypeError. For as-of / temporal fact queries call the REST
endpoint directly (GET /v1/facts?as_of=…&min_confidence=…&include_superseded=…) or
GET /v1/facts/timeline.
Facts / Beliefs need enrichment on self-host
On a content-only self-hosted instance the Facts and Beliefs layers stay empty — they are
LLM-derived and require enrichment configured. events/episodes/recall work regardless. See
Self-hosting defaults.
Trigger an async synthesis pass:
client.build_episodes(scope="org:acme/user:alice")
client.build_beliefs(scope="org:acme/user:alice")
client.synthesize(scope="org:acme/dept:eng", topics=["sales_process"])synthesize() takes no force= parameter
synthesize(scope, *, topics=None) is the full signature — there is no force= keyword.
Forget
client.forget(
scope="org:acme/user:alice",
layers=["beliefs"],
predicate="is_likely_to_renew",
cascade="derived_only",
reason="User retracted speculation",
)forget() targets by field
forget() takes explicit targeting fields (there is no selector= dict or idempotency_key=):
forget(scope, *, layers=None, about_subject=None, about_entity=None,
predicate=None, memory_ids=None, cascade='derived_only',
confirm_all=False, reason=None, from_preview_id=None)Use predicate=, about_subject=, about_entity=, or memory_ids=[…] — not selector={…}.
The audit note field is reason (not audit_note), and idempotency_key is not accepted on
forget().
To purge specific records, pass their ids:
client.forget(scope="org:acme/user:alice", memory_ids=["evt_01HX...", "fact_01HX..."])If a storage backend fails mid-delete the server returns 502 FORGET_BACKEND_FAILED rather than
reporting success — deletes are idempotent, so retry the same call. For GDPR reference-counted erasure
use the /v1/erasures family directly — see Erasures.
Audit
client.audit_list(actor="user:alice", capability="forget.gdpr", limit=100)
client.audit_verify(audit_id="audit_01HX...", body="<canonicalized JSON>")Admin
client.layer_stats() # GET /v1/admin/layers/stats (experimental)Exception hierarchy
All HTTP errors raise a subclass of cortexdb.v1.V1Error:
from cortexdb.v1 import (
V1Error, # base
V1APIError, # generic API error envelope
V1AuthError, # 401
V1PolicyDeniedError, # 403
V1RateLimitError, # 429
V1NotConfiguredError, # 503
V1ConnectionError, # network failure
V1TimeoutError, # timeout
)
try:
client.experience(scope="...", text="...", observed_at="...", idempotency_key="...")
except V1PolicyDeniedError as e:
print(f"denied: {e.error_code} — {e.message}")
print(e.details) # capability/tier context, when the server provides it
except V1RateLimitError:
time.sleep(1.0)API error attributes
V1APIError and its subclasses carry status, error_code, message, request_id, details, and
retriable. Read policy context on V1PolicyDeniedError from error_code and details.
What 0.12.0 sees from the server
Checked with cortexdbai 0.12.0 against the current server (no client change is needed):
- A
wait="indexed"(or"consolidated") write that runs out of the server's wait budget raisesV1APIErrorafter about 30 s — before the client's own 60 s timeout — withstatus408,error_codeWAIT_TIMEOUT,retriableTrue anddetails.status_url(details.event_idsfromexperience_bulk). The write is durable: pollwrite_status, or re-send with the sameidempotency_key(see Experience). - A
forget(or erasure) that meets409 AUTHORIZATION_STATE_CHANGEDraises a plainV1APIErrorwithretriableTrue, notV1PolicyDeniedError. Nothing was deleted; retry after a second. - There is no method for
GET /v1/events/{id}, so its?scope=filter has no SDK argument; call the route directly (see Events).
Async variant
AsyncV1Client is backed by httpx.AsyncClient and every method on it is async. It is not a
drop-in mirror of V1Client.
from cortexdb.v1 import AsyncV1Client
async with AsyncV1Client(
api_url="https://api-v1.cortexdb.ai",
actor="user:alice",
bearer=tok,
) as client:
pack = await client.recall(scope="org:acme/user:alice", view="holistic")
ans = await client.answer(scope="org:acme/user:alice", question="Did Acme renew?")The async client is a subset: 32 methods vs 53
The sync client has 53 methods and the async client 32. Twenty-two
methods exist only on V1Client and raise AttributeError on the async client — notably every layer
read and the retention / WAL operator methods:
events · facts · beliefs · episodes · understanding · belief_why · build_beliefs ·
build_episodes · synthesize · audit_list · audit_verify · index_audit · layer_stats ·
write_status · close · retention_policy_get · retention_policy_set · retention_policy_delete ·
retention_status · retention_sweep · wal_status · wal_compact
Close the async client with aclose() (there is no close()), or use async with as above. If
you need the layer reads in an async program, call them on a V1Client in a thread executor or use the
REST endpoints directly. Also note AsyncV1Client.recall() takes a narrower
argument set than the sync one — it has no include=, budgets=, temporal= or filters= parameters
(it does take language=, scripts= and query_variants=).
Method index
Capabilities, chunks and language fields
version()returns{version, crate_version, …, capabilities}andcapabilities()returns just the capability list (GET /v1/admin/version). Gate every new request field on it: a server that doesn't list a capability refuses its fields with422 INVALID_BODY.event_chunks(event_id)shows how a long event was chunked (GET /v1/admin/events/{id}/chunks):chunk_count,chunker_version,chunks,language,scripts,source_blob_ids,stored_text_bytes.iter_matched_fragments(pack)andfragment_text(text, fragment)(import fromcortexdb.v1, not the package top level) walk a recall pack'smatched_fragmentsand slice each span out of the event's text; an excerpted item yields no span.experience()takeslanguage=,scripts=,timezone=andrefers_to=;recall()takeslanguage=,scripts=andquery_variants=— see Languages & Multilingual Memory, Experience and Recall.delete_blob(blob_id, missing_ok=True)returns{…, "already_deleted": true}for a blob that is already gone instead of raising404 NOT_FOUND.
Retention and WAL operator methods
retention_policy_get / retention_policy_set / retention_policy_delete / retention_status /
retention_sweep / wal_status / wal_compact wrap the operator routes /v1/admin/retention,
/v1/admin/retention/policies, /v1/admin/retention/sweep, /v1/admin/wal and /v1/admin/wal/compact.
Expiry is irreversible erasure, so the
server defaults dry_run to true on each of them. Sync client only.
The sections above cover the common paths. The full V1Client surface also
includes these, several of which map to endpoints documented elsewhere in this reference:
| Area | Methods |
|---|---|
| Compose | compose → POST /v1/compose |
| Bi-temporal records | query_claims · claim_history · list_conflicts · get_conflict · resolve_conflict → Claims & Conflicts |
| Layer history | belief_history · concept_history |
| Erasure | erase · erasure_preview · forget_preview → /v1/erasures |
| Import | import_data · import_status → /v1/import |
| Blobs | upload_blob · delete_blob → /v1/blobs |
| Beliefs (write) | create_belief |
| Vocabularies | update_vocabulary → /v1/vocabularies |
| Auth | signup · mint_token(subject=, scope=, caps=, scopes=) · whoami → /v1/auth |
| Server | version · capabilities → /v1/admin/version |
| Documents | event_chunks → GET /v1/admin/events/{id}/chunks |
| Audit | audit_chain · audit_chain_verify |
| Write status | write_status — "did my write land?" by idempotency_key or event_id |
| Lifecycle | close (sync) · aclose (async) |