Skip to content

API

The xmemory REST API is what the Python and TypeScript SDKs call under the hood. You can also call it directly with curl, fetch, or any HTTP client.

For MCP-based access (no HTTP calls needed), see the MCP guide.

All endpoints live under https://api.xmemory.ai, accept JSON, and return JSON. Every request except /healthz must send your xmemory API key as a Bearer token: Authorization: Bearer <your API key>.

Get your API key from the xmemory console. Copy and securely store it — never share your API key publicly.


Every endpoint wraps its result in a standard envelope:

{
"ids": ["<created-resource-id>"],
"items": [ { "": "the actual payload" } ],
"errors": [],
"console_url": "https://console.xmemory.ai/…"
}

On a successful response, the per-endpoint shapes documented below (write_id, reader_result, data_schema, …) are the object inside items[0] — a raw HTTP caller must read them there, not at the top level. ids collects created resource IDs, errors any non-fatal errors, and console_url links to the relevant view in the console. (The Python and TypeScript SDKs unwrap this for you.) Schema-evolution endpoints report failures differently: they return a structured error body at the top level rather than in this envelope (see Structured errors).


Write something, then read it back — two curl calls:

Terminal window
# Write
curl -X POST https://api.xmemory.ai/instances/$XMEM_INSTANCE_ID/write \
-H "Authorization: Bearer $XMEM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Alice Johnson works at Acme Corp. Her email is alice@acme.com.",
"extraction_logic": "deep"
}'
# Read
curl -X POST https://api.xmemory.ai/instances/$XMEM_INSTANCE_ID/read \
-H "Authorization: Bearer $XMEM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "What is Alice'\''s email?"
}'
# → {"ids": [], "items": [{"reader_result": {"answer": "alice@acme.com"}}], "errors": [], "console_url": "…"}

The rest of this page covers every endpoint in detail. The data-plane endpoints (write, read, extract, describe) come first; if you don’t have an instance yet, jump to Clusters and Instances to create one.


Send raw text; xmemory extracts typed objects from it and persists them. write blocks until committed; write_async returns immediately and you poll write_status for completion. Both endpoints alternatively accept structured_mutations for deterministic, extraction-free edits — see Structured writes.

Endpoints

POST/instances/{instance_id}/writePersist data — extract structured objects from text, or apply explicit structured mutations (blocks until committed)
textstring
Free-form text to extract from. Exactly one of text / structured_mutations must be provided.
structured_mutationsarray
Ordered list of deterministic, LLM-free create/update/delete mutations of objects and relations — see Structured writes. Mutually exclusive with text.
extraction_logicstring
"fast" (default) or "deep". Text writes only. Defaults to "fast".
trace_idstring
Caller-supplied ID echoed back on the response for log correlation.
session_idstring
Session ID for grouping related calls in tracing.
use_diff_engineboolean
Override the instance's diff-engine setting for this write. Text writes only.
write_idstring
ID of the committed write.
cleaned_objectsobject
The stored objects, as { objects: [...], relations: [...] }.
changesobject
What the write did, grouped into created / updated / deleted (each { objects: [...], relations: [...] }; updated entries carry per-field old/new values).
trace_idstring
Echoed trace id for log correlation.
{
"write_id": "...",
"cleaned_objects": { "objects": [], "relations": [] },
"changes": { "created": { "objects": [], "relations": [] }, "updated": { "objects": [], "relations": [] }, "deleted": { "objects": [], "relations": [] } },
"trace_id": "..."
}
POST/instances/{instance_id}/write_asyncSame write pipeline as write, but returns immediately instead of blocking
textstring
Free-form text to extract from. Exactly one of text / structured_mutations must be provided.
structured_mutationsarray
Ordered list of deterministic, LLM-free create/update/delete mutations — see Structured writes. Mutually exclusive with text.
extraction_logicstring
"fast" (default) or "deep". Text writes only. Defaults to "fast".
trace_idstring
Caller-supplied ID echoed back for log correlation.
session_idstring
Session ID for grouping related calls in tracing.
use_diff_engineboolean
Override the instance's diff-engine setting for this write. Text writes only.
write_idstring
ID to poll with writeStatus. Do not read immediately after an async write — the data may not be committed yet.
{ "write_id": "..." }
POST/instances/{instance_id}/write_statusCheck whether an async write has finished
write_idstringrequired
ID returned by writeAsync.
write_statusstring
One of queued, processing, completed, failed, not_found — plus the intermediate two-phase-pipeline states extracting, extracted, applying. Treat anything other than completed / failed / not_found as still in flight.
error_detailstring
Human-readable failure detail when write_status is failed; otherwise null.
errorobject
Structured error (code, message) when the write failed; otherwise null.
completed_atstring
ISO-8601 timestamp when the write finished; null until then.
{
"write_id": "...",
"write_status": "completed",
"error_detail": null,
"error": null,
"completed_at": "2025-03-16T12:34:56Z"
}

When you already know exactly what to store, skip extraction entirely: pass structured_mutations instead of text (exactly one of the two — the request is rejected if both or neither are present). Each mutation is applied deterministically, with no LLM involved, making structured writes fast and exactly repeatable.

A mutation is tagged object_mutation or relation_mutation and carries exactly one operation — create, update, or delete:

Terminal window
curl -X POST https://api.xmemory.ai/instances/$XMEM_INSTANCE_ID/write \
-H "Authorization: Bearer $XMEM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"structured_mutations": [
{
"object_mutation": {
"object_type": "person",
"create": {
"key": { "email": "alice@acme.com" },
"values": { "name": "Alice Johnson" }
}
}
},
{
"relation_mutation": {
"relation_type": "works_at",
"create": {
"endpoints": [
{ "object_name": "person", "key": { "email": "alice@acme.com" } },
{ "object_name": "company", "key": { "name": "Acme Corp" } }
]
}
}
}
]
}'

Addressing objects. key identifies an object by its user-defined primary-key fields (as above) — or, for update / delete, by { "xuid": "..." } (the server-generated unique ID returned in changes). create never takes an xuid; it is generated server-side.

Object operations.

  • createkey (primary-key fields) plus values (the remaining fields).
  • updatekey plus values; a null value clears that field.
  • deletekey only.

Relation operations.

  • createendpoints (one per relation role: object_name + that object’s key) plus optional own-field values.
  • update — address by endpoints, or by key: { "xuid": ... } when endpoints are ambiguous; values as for objects.
  • delete — match by endpoints (a subset is allowed) or key; deleting more than one matched row requires "allow_bulk_delete": true.

Ordering and batches. Mutations apply in list order, and later mutations may reference objects created earlier in the same batch (e.g. create a person, then a relation pointing at it). Multiple mutations of the same object or relation in one batch follow a strict contract: compatible sequences compose (create then update applies the update to the created row; update then delete nets out to a delete; duplicate deletes are no-ops), while contradictory ones — create then delete, or anything after a delete — are rejected with a “split into separate writes” error rather than silently reordered.

Response. The same shape as a text write; changes reports exactly what was created / updated / deleted, with per-field old/new values on updates. write_async accepts structured_mutations identically — poll writeStatus and read changes from the completed status.

Availability. Structured writes can be disabled per deployment; when off, the request fails with "Structured writes are not enabled on this deployment."


Ask a question in natural language — xmemory searches the instance’s stored objects and returns a synthesized answer.

Endpoints

POST/instances/{instance_id}/readQuery the instance in natural language
querystringrequired
Natural-language question.
modestring
"single-answer" (default), "xresponse", or "raw-tables". Defaults to "single-answer".
scopeobject
Restrict the read to specific objects: { "objects": [...], "relations_scope": "no_relations" | "all_relations" }. Defaults to no_relations (objects only). See Scoped reads.
return_sqlboolean
Include the executed SQL in the response (sql). Defaults to false.
skip_suggestion_captureboolean
Skip recording read-gap signals for the schema-suggestion engine. Defaults to false.
trace_idstring
Caller-supplied ID echoed back for log correlation.
session_idstring
Session ID for grouping related calls in tracing.

The reader_result shape depends on the mode:

Mode reader_result Best for
single-answer {"answer": "Bob Lee joined last Monday."} Natural-language answers
xresponse {"objects": [...], "relations": [...]} Structured data
raw-tables {"tables": [...]} Raw SQL results

Also returned: sql (the executed SQL, when return_sql: true), pending_suggestions (count of pending schema improvements), and trace_id.


Restrict a query to a specific set of objects in your instance, limiting data access to only what you specify. Useful for multi-tenant systems, privacy controls, and compliance requirements.

Add a scope field to the /read endpoint:

{
"query": "What is their email?",
"scope": {
"objects": [
{
"type": "Person",
"key": {"email": "alice@example.com"}
}
],
"relations_scope": "no_relations"
}
}

List of one or more objects to query. Each object has:

  • type (string): The object type (e.g., "Person", "Order").
  • key (object): Identify the object by its primary key fields (e.g., {"email": "alice@example.com"}).

relations_scope (optional, default: "no_relations")

Section titled “relations_scope (optional, default: "no_relations")”

Control whether to include relations that these objects are part of:

  • "no_relations": Only the specified objects.
  • "all_relations": Include relations these objects are part of.
Terminal window
curl -X POST https://api.xmemory.ai/instances/your-instance-id/read \
-H "Authorization: Bearer $XMEM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "What is their email and phone number?",
"scope": {
"objects": [
{"type": "Person", "key": {"name": "Alice Johnson"}}
]
}
}'
Terminal window
curl -X POST https://api.xmemory.ai/instances/your-instance-id/read \
-H "Authorization: Bearer $XMEM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "Which projects do these people work on together?",
"scope": {
"objects": [
{"type": "Person", "key": {"email": "alice@example.com"}},
{"type": "Person", "key": {"email": "bob@example.com"}}
],
"relations_scope": "all_relations"
}
}'

Semantics: The scope is limited to the two specified Person objects. With "relations_scope": "all_relations", relations these Person objects are part of become visible.

If the scope is invalid, the API returns HTTP 400:

{
"status": "error",
"error_message": "Unknown scope object type 'Foo'; valid types are Person, Project"
}

Preview the objects a write would produce — same extraction pipeline, but nothing is stored.

Endpoints

POST/instances/{instance_id}/extractExtract structured objects from text without storing them — preview what a write would produce
textstringrequired
Free-form text to extract from.
extraction_logicstring
"fast" (default) or "deep". Same parameters as write. Defaults to "fast".
trace_idstring
Caller-supplied ID echoed back for log correlation.
session_idstring
Session ID for grouping related calls in tracing.
objects_extractedobject
The extracted (but not stored) objects, as { objects: [...], relations: [...] }.
trace_idstring
Echoed trace id for log correlation.
{ "objects_extracted": { "objects": [], "relations": [] }, "trace_id": "..." }

Return the agent-facing tool descriptions for an instance, enriched with its live schema.

Endpoints

GET/instances/{instance_id}/describeReturn agent-facing tool descriptions enriched with the instance's actual schema
instance_idstring
The instance the tools operate on.
instance_namestring
Human-readable instance name.
schema_summarystring
Human-readable description of the instance's schema, suitable for including in an LLM system prompt.
toolsarray
Tool descriptions; only includes tools the caller has permission to use.
{
"instance_id": "...",
"instance_name": "contacts",
"schema_summary": "This instance tracks contacts with name, email, ...",
"tools": [
{
"name": "write",
"description": "Extract structured data from text and persist it.",
"when_to_use": "When you need to store new facts ...",
"parameters": [
{ "name": "text", "type": "string", "description": "...", "required": true },
{ "name": "extraction_logic", "type": "string", "description": "...", "required": false, "enum": ["fast", "deep"], "default": "fast" }
],
"http_method": "POST",
"http_path": "/instances/.../write"
}
]
}

Clusters are the top-level container that holds instances. List the clusters you can access, or fetch one by ID.

Endpoints

GET/clustersList all clusters accessible to the authenticated user

An array of clusters, each { id, org_id, name, description }.

Terminal window
curl https://api.xmemory.ai/clusters \
-H "Authorization: Bearer $XMEM_API_KEY"
GET/clusters/{cluster_id}Get a single cluster by ID

A single cluster { id, org_id, name, description }.

Terminal window
curl https://api.xmemory.ai/clusters/$XMEM_CLUSTER_ID \
-H "Authorization: Bearer $XMEM_API_KEY"

Turn a plain-language description into a typed xmemory schema — the first step before creating an instance.

Endpoints

POST/clusters/{cluster_id}/instances/generate_schemaGenerate an xmemory schema from a plain-language description
schema_descriptionstringrequired
Plain-language description of the data to track.
current_yml_schemastring
Existing schema to refine; runs the enhance path and returns a structured migration plan so the change can be applied safely (renames preserve data, etc.).
data_schemaobject
The generated schema. When current_yml_schema is omitted (create-from-scratch), only data_schema is returned.
migration_planobject
Structured plan to apply the change; pass it to the dry-run and update endpoints. See Schema evolution.
summarystring
Human-readable summary of the change.
warningsarray
Non-fatal warnings raised while generating or enhancing the schema.
repair_logarray
Automatic corrections applied to make the generated schema valid.
{
"data_schema": { "...": "..." },
"migration_plan": { "ops": [ { "op_type": "rename_field", "object_name": "person", "old_name": "mail", "new_name": "email" } ] },
"summary": "Rename person.mail to person.email.",
"warnings": [],
"repair_log": []
}

An instance is a memory store with a typed schema, living inside a cluster — create one before you write or read. These endpoints manage its whole lifecycle: create, list, fetch, update its schema or metadata, and delete.

Endpoints

POST/clusters/{cluster_id}/instancesCreate an instance from a generated schema
namestringrequired
Instance name.
descriptionstring
Description.
instance_schemaobjectrequired
Use "yml": { "value": "..." } for YAML schemas, or "json_schema": { "value": "..." } for JSON.
schema_descriptionstring
Plain-language description stored with the instance.
generate_trace_idstring
Trace id linking this instance to the generateSchema call that produced its schema.
idstring
The new instance ID. Save it — you'll pass it with every subsequent call.
cluster_idstring
The cluster the instance was created in.
namestring
Instance name.
descriptionstring
Instance description, if set.
data_schemaobject
The instance's active schema.
{
"id": "...",
"cluster_id": "...",
"name": "contacts",
"description": null,
"data_schema": { "...": "..." }
}
GET/instancesList all instances accessible to the authenticated user

An array of instances (see getInstance for the per-item shape).

Terminal window
curl https://api.xmemory.ai/instances \
-H "Authorization: Bearer $XMEM_API_KEY"
GET/instances/{instance_id}Get a single instance by ID
idstring
Instance ID.
cluster_idstring
The cluster the instance belongs to.
namestring
Instance name.
descriptionstring
Instance description, if set.
created_onstring
ISO-8601 creation timestamp.
created_by_emailstring
Email of the account that created the instance.
data_schemaobject
The instance's active schema.
GET/instances/{instance_id}/schemaGet the current schema of an instance
data_schemaobject
The instance's active schema (objects, fields, relations).
PUT/instances/{instance_id}/schemaUpdate the schema of an existing instance
instance_schemaobjectrequired
The new schema. A purely additive change (new objects/fields/relations) needs only this field.
migration_planobject
Required for a non-additive change (rename, remove, type change); pass the plan from generateSchema.
confirm_destructiveboolean
Set true for any op that drops data. Defaults to false.
migration_idstring
Present when a migration ran (a non-additive change).
prior_versioninteger
Schema version before this update.
new_versioninteger
Schema version after this update.
migration_warningsarray
Non-fatal warnings emitted by the migration.

A non-additive change sent without a plan is rejected with non_additive_change_requires_plan. An op that drops data without confirm_destructive: true returns destructive_confirmation_required. See Structured errors.

PUT/instances/{instance_id}Update instance metadata (name and description)
namestring
New instance name.
descriptionstring
Updated description.
idstring
Instance ID.
namestring
Updated instance name.
descriptionstring
Updated description.
DELETE/instances/{instance_id}Delete an instance

A list of deleted instance IDs.


xmemory can change a live schema without losing data. There are two paths, both ending in an atomic migration with an audit record: a direct migration you drive yourself, and a suggestion engine that proposes improvements from real read traffic. Additive-only callers are unaffected — the endpoints above keep working as before.

Endpoints

POST/instances/{instance_id}/migrations/dry_runPreview a migration without applying it
instance_schemaobjectrequired
The proposed new schema (same body shape as the schema update).
migration_planobject
Plan for a non-additive change; pass the plan from generateSchema.
confirm_destructiveboolean
Set true for any op that drops data. Defaults to false.
statementsarray
The planned DDL statements.
current_versioninteger
The instance's current schema version (the migration would advance it).
plan_summaryobject
Counts of ops by type: { count_by_op_type, total }.
requires_metadata_syncboolean
Whether applying would also resync derived metadata.
{
"status": "ok",
"instance_id": "...",
"current_version": 4,
"statements": ["ALTER TABLE person RENAME COLUMN mail TO email"],
"warnings": [],
"plan_summary": { "count_by_op_type": { "rename_field": 1 }, "total": 1 },
"requires_metadata_sync": true
}

A non-additive change without a plan returns non_additive_change_requires_plan; a data-dropping op without confirmation returns destructive_confirmation_required. See Structured errors.

GET/instances/{instance_id}/migrationsList applied migrations, newest first
limitinteger
Range 1–200. Defaults to 50.
before_idstring
UUID pagination cursor.
include_yamlboolean
Adds before/after schema snapshots. Defaults to false.
itemsarray
Migration records (see getMigration for the per-item shape). source is direct or suggestion_engine.
next_before_idstring
Pass as before_id to fetch the next (older) page; null on the last page.
has_moreboolean
Whether more records exist beyond this page.
{
"status": "ok",
"items": [
{ "id": "...", "applied_at": "...", "source": "suggestion_engine", "decided_by": "...",
"prior_version": 3, "new_version": 4, "ops": [ ], "ops_summary": { } }
],
"next_before_id": null,
"has_more": false
}
GET/instances/{instance_id}/migrations/{migration_id}Get a single migration record
include_yamlboolean
Adds the before/after schema snapshots (yaml_before / yaml_after). Defaults to false.
idstring
Migration record ID.
applied_atstring
ISO-8601 timestamp the migration was applied.
sourcestring
direct or suggestion_engine.
prior_versioninteger
Schema version before the migration.
new_versioninteger
Schema version after the migration.
opsarray
The migration operations that were applied.
ops_summaryobject
Counts of ops by type.
yaml_beforestring
Schema YAML before the migration (only when include_yaml: true).
yaml_afterstring
Schema YAML after the migration (only when include_yaml: true).

Suggestion engine — review → decide → apply

Section titled “Suggestion engine — review → decide → apply”

The engine surfaces a single rolling proposal per instance, derived on demand from accumulated read-gap signals. The minimum flow is three calls.

Endpoints

POST/instances/{instance_id}/suggestions/reviewReturn the consolidated proposal and a proposal_version concurrency token
session_idstring
Body may be {} or { "session_id": "..." }.
proposalobject
Holds proposal_version (concurrency token), schema_version, and items.
{
"status": "ok",
"instance_id": "...",
"proposal": {
"proposal_version": "ab12...",
"schema_version": 4,
"items": [
{ "item_fingerprint": "fp1", "op": { "op_type": "add_field", "object_name": "person", "field_name": "phone", "field_type": "str" },
"rationale": "queried but missing", "frequency": 3, "depends_on": [], "evidence_query_samples": ["what is bob's phone"] }
]
}
}

When a migration is already running, the response is { "status": "evolution_in_progress", "retry_after_seconds": 5 } — back off and retry.

POST/instances/{instance_id}/suggestions/decideRecord an accept / reject / defer per item in one batch
proposal_versionstringrequired
The proposal_version from review.
decisionsarrayrequired
One { "item_fingerprint": "...", "decision": "accept" | "reject" | "defer" } per item.
session_idstring
Session ID for tracing.

The recorded decisions, advisory dependency warnings, and a next_proposal_version you can pass straight to apply.

{
"proposal_version": "ab12...",
"decisions": [ { "item_fingerprint": "fp1", "decision": "accept" } ]
}

A proposal_version that no longer matches the current proposal returns stale_proposal_version; malformed decisions return invalid_decision_input. See Structured errors.

POST/instances/{instance_id}/suggestions/applyCommit accepted decisions as one migration
proposal_versionstringrequired
The next_proposal_version from decide.
session_idstring
Session ID for tracing.

On success, { "status": "ok", "migration_id": "...", "prior_version": 4, "new_version": 5, "applied_items": ["fp1"], "summary": "..." }, or { "status": "nothing_to_apply" } if no accepted items remained.

{ "status": "ok", "migration_id": "...", "prior_version": 4, "new_version": 5, "applied_items": ["fp1"], "summary": "..." }

A stale token returns stale_proposal_version; an unmet dependency returns dependency_closure_failed; a failed commit returns apply_failed. See Structured errors.

POST/instances/{instance_id}/pending-feedback/discardClear the pending schema-improvement backlog for this instance
rows_discardedinteger
Number of pending feedback signals that were discarded.
instance_idstring
The instance whose backlog was cleared.
{ "status": "ok", "instance_id": "...", "rows_discarded": 12 }

Bulk-supersedes the pending schema-improvement backlog. Idempotent — discarded signals never resurface, and calling it with nothing pending is a safe no-op.

Schema-evolution endpoints return a structured error body with a 4xx/5xx status:

{ "status": "error", "error_type": "stale_proposal_version", "error_message": "...", "details": { ... } }

error_type is one of: non_additive_change_requires_plan, destructive_confirmation_required, stale_schema_version, migration_precondition_failed, migration_execution_failed, migration_not_found, stale_proposal_version, dependency_closure_failed, invalid_decision_input, apply_failed, instance_not_initialised. The SDKs surface error_type as a structured error code.

No backfill of historical data into new fields; no rollback of a committed migration (atomic abort on failure is the safety story); identity tightening (change_object.new_primary_key, change_relation.new_keys) is relax-only.


Endpoints

GET/healthzReturns 200 if the API is reachable (no authentication required)
Terminal window
curl https://api.xmemory.ai/healthz

All endpoints return an error response on failure with the appropriate HTTP status code (400, 401, 404, 500).