PAK API reference

Exactly what a personal access key (mhk_… — see the guide) can call, and the shape of each request and response. There are two surfaces:

  • MCP tools — JSON-RPC over HTTP at https://api.memhub.xtrace.ai/mcp-server/mcp. The natural interface for agents; ~38 tools.
  • REST — plain HTTP under https://api.memhub.xtrace.ai/v1.

Auth, scopes, and the envelope

  • Every call sends Authorization: Bearer mhk_….
  • REST endpoints take an X-Org-Id header to choose the org; omit it to use the key's pinned org (or your default org).
  • Scopes — read calls need memory:read; anything that writes needs memory:write. A read-only key is rejected from every write, on both MCP and REST.
  • Rate limit — one seat's throughput. Over it → 429 with a Retry-After header.
  • REST envelope — responses are wrapped: { "code": 0, "msg": "ok", "data": <payload> } (code 0 = success). The shapes below are the data payload.

What a PAK can not call

  • Mint/list/revoke keys (/v1/developer/access-tokens) — JWT (dashboard) only; a key can't mint another key.
  • The Memory-API product's stored data/v1/developer/mem-manager/{memories, conversations,exports,groups} are dashboard-only; read that product's data with an xtk_ data-plane key, not a PAK.
  • Org settings, billing, membership, other people's private data.

MCP tools

Call over JSON-RPC — POST https://api.memhub.xtrace.ai/mcp-server/mcp with {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"<tool>","arguments":{…}}} and both Accept: application/json, text/event-stream + Content-Type: application/json headers. The result is the tool's return object.

Every tool also accepts an optional org_id argument (omitted from the tables below) to target a specific org. Most also accept an optional agent_brain_id to scope the call to one brain instead of your workspace memory. Required arguments are marked *.

Read tools — need memory:read

ToolArgumentsWhat it returns
search_memoryquery*, memory_type, top_k, agent_brain_id, tags, match, created_after, created_beforeRanked memory items (facts / artifacts / episodes / documents) with scope + items[].
search_brainsquery*, top_k, folderWhich agent brains are most relevant to a topic.
search_all_brainsquery*, memory_type, top_k, folderMatching items across every brain you can access.
recall_directivestool, args, output, entities, task, repo, already_fired, session_id, limitSituated lessons/procedures relevant to what you're doing.
list_tagsagent_brain_idThe tag vocabulary + counts in a scope.
get_artifactartifact_id*, agent_brain_idFull content of one artifact.
get_artifact_lineageartifact_id*, agent_brain_idEvery version in the chain + derived_from links.
diff_artifact_versionsfrom_id*, to_id*, agent_brain_idUnified diff between two artifact versions.
get_brain_overviewagent_brain_id*The compiled overview of a brain.
list_agent_brainscreated_after, created_beforeBrains you can access, newest first.
list_agent_brain_accessagent_brain_id*Who has access to a brain and at what level.
list_brain_foldersYour brain-grouping folders.
list_workspacesWorkspaces you can use.
list_orgsOrgs you belong to + the default this connection acts in.
list_teammatesActive members of your org (excluding you).
list_skillsagent_brain_id, limitApproved skills in a scope, with SKILL.md text.
get_skillskill_id*, agent_brain_idOne skill's full SKILL.md by id or name.
get_skill_fileskill_id*, path*, agent_brain_idOne file from a skill's bundle, base64-inline.

Write tools — need memory:write

ToolArgumentsWhat it does
add_memoryuser_message*, assistant_message, conversation_id, message_id, agent_brain_idSave a conversation turn into team memory.
import_conversationmessages*, conversation_id, title, source_platform, agent_brain_id, namespaceImport a whole conversation in one call.
save_artifactname*, content, artifact_type, agent_brain_id, parent_id, rationale, tags, files, entrypointSave a document/spec as an artifact (versioned).
ingest_document_from_urlurl*, name, agent_brain_idIngest a public URL as a searchable document.
tag_artifactartifact_id*, add_tags, remove_tags, agent_brain_idAdd/remove tags on an artifact (no new version).
tag_documentdocument_id*, add_tags, remove_tags, agent_brain_idAdd/remove tags on an ingested document.
delete_artifactartifact_id*, agent_brain_idDelete an artifact + its whole version history.
delete_factfact_id*, agent_brain_idRetract a fact so it stops surfacing.
delete_episodeepisode_id*, agent_brain_idDelete an episode.
copy_memory_to_brainitem_ids*, agent_brain_id*Copy existing memory into a brain.
create_skillname*, trigger*, skill_md*, scripts, assets, summary, agent_brain_idSave an executable SKILL (Agent-Skills format).
add_skill_to_brainskill_id*, agent_brain_id*, source_agent_brain_id, refreshCopy a skill into a brain.
delete_skillskill_id*, agent_brain_idDelete a skill.
create_agent_brainname*, description, workspace_id, categoryCreate an agent brain.
refresh_brain_overviewagent_brain_id*Regenerate a brain's overview now.
share_agent_brainagent_brain_id*, teammate_user_id*, permissionShare a brain / change a teammate's access.
create_workspacename*Create a workspace.
create_brain_foldername*Create a folder for grouping brains.
move_brain_to_folderagent_brain*, folder*File a brain into a folder.
remove_brain_from_folderagent_brain*Take a brain out of its folder.

REST endpoints

  • Base https://api.memhub.xtrace.ai/v1
  • Headers Authorization: Bearer mhk_…, plus X-Org-Id: <org-uuid> to choose the org (omit it if the key is pinned to one org, or you have a single/default org).
  • Envelope every response is { "code": 0, "msg": "ok", "data": … }. The examples below show the data.

Team memory — facts, artifacts, brains

The same read/write of your team's memory the MCP tools give you, as plain HTTP. Requires an active company-org membership.

EndpointScopeIn → out
GET /v1/team/factsreadquery filters → fact[]
POST /v1/team/facts/searchreadsearch body → { hits, scopes }
PATCH /v1/team/facts?logical_id=eq.<id>write{ "content": "…" }fact
DELETE /v1/team/facts?logical_id=eq.<id>write— → { deleted, scope }
GET /v1/team/artifactsreadquery filters → artifact[]
POST /v1/team/artifacts/searchreadsearch body → { hits, scopes }
PATCH /v1/team/artifacts?logical_id=eq.<id>writepatch body → artifact
DELETE /v1/team/artifacts?logical_id=eq.<id>write— → { deleted, scope }
GET /v1/team/cbsreadquery filters → brain[]
POST /v1/team/cbswritecreate body → brain
POST /v1/team/cbs/{cb_id}/searchreadsearch body → { facts, artifacts, … }

Editing, deleting, or sharing a brain over REST stays dashboard-only — do those over MCP (share_agent_brain) or in the app.

Filtering the GET lists

PostgREST-style querystrings:

?status=eq.active            # filter — ops: eq  in.(a,b)  is.null  gte  lte  between.(a,b)  like.(*)
?order=updated_at.desc       # sort
?select=logical_id,content   # projection (artifacts default to a light set; opt into heavy cols here)
?limit=50&offset=0           # page — limit 1–200 (default 50)

Filters AND across columns, OR within one (in.(…)). PATCH/DELETE need a key filter — ?logical_id=eq.<uuid> (facts/artifacts) or ?cb_id=eq.<uuid> (brains). Facts default to your active long-term memory (status=active, no brain); artifacts to chain heads (is_latest=true). Pass ?cb_id=eq.<cb> to read a brain's items.

Search a resource

POST …/facts/search and …/artifacts/search take the same body:

{
  "query": "onboarding decisions",
  "top_k": 10,
  "threshold": 0.0
}

query is required (1–4000 chars); top_k 1–100 (default 10); threshold −1.0…1.0 (default 0.0), where score = 1 − cosine_distance. Filters go in the querystring.

Response:

{
  "hits": [
    {
      "type": "fact",
      "logical_id": "…",
      "content": "…",
      "score": 0.82,
      "workspace_id": "…",
      "cb_id": null
    }
  ],
  "scopes": []
}

type is fact | artifact | episode | document; document hits also carry document_id, chunk_id, chunk_level.

A fact

{
  "logical_id": "uuid",
  "content": "…",
  "status": "active",             // active | superseded | retracted | expired
  "origin": "user",               // user | descriptor
  "origin_kind": "conversation",  // conversation | notion | granola | web | research
  "fact_type": "preference",
  "cb_id": null,                  // set when the fact lives in a brain
  "is_copy": false,
  "created_at": "2026-08-05T12:00:00Z",
  "updated_at": "2026-08-05T12:00:00Z"

  // also present: source_role, change_type, change_reason, conv_id, episode_id, run_id,
  // policy_id, policy_rule_id, superseded_by, supersedes, primary_source_id,
  // root_artifact_id, source_artifact_id, consolidated_at, event_date.
  // directive facts add: trigger_entities[], steps[], scope, observation_count,
  // last_confirmed_at.
}

Patch a fact with { "content": "…" }.

An artifact

{
  "logical_id": "uuid",
  "name": "Billing spec",
  "artifact_type": "spec",
  "version": 3,
  "is_latest": true,
  "summary": "…",
  "tags": ["billing"],
  "cb_id": null,
  "is_copy": false,
  "created_at": "…",
  "updated_at": "…"

  // light set also has: source_role, document_id, root_logical_id, parent_logical_id,
  // rationale, origin_kind, descriptor_fact_ids[], primary_source_id, policy_id,
  // policy_rule_id, run_id.
  // ?select= opts into heavy cols: content, workspace_id, org_id, user_id,
  // source_event_ids[], source_dia_ids[], conv_id, episode_id, event_date,
  // files ([{ path, size, content_type }]), entrypoint.
}

Patch an artifact with any of { "name", "summary", "rationale", "add_tags": [], "remove_tags": [] } (all optional).

Brains — create & search

Create — POST /v1/team/cbs:

{
  "name": "Payments repo",
  "category": "repo",       // repo | research | skills | meetings | integration | documents | other
  "description": "…",       // optional
  "workspace_id": "uuid"    // optional — omit for your default workspace
}

Search a brain — POST /v1/team/cbs/{cb_id}/search (cb_id in the path):

// request
{
  "query": "how does refund work",
  "top_k": 10,
  "threshold": 0.0,               // 0.0…1.0 here
  "types": ["fact", "artifact"]   // optional — default all of fact/artifact/episode/document
}

// response data
{
  "facts": [], "artifacts": [], "episodes": [], "documents": [],
  "context_markdown": null,
  "char_count": 0
}

A brain object (GET / create response):

{
  "cb_id": "uuid",
  "name": "Payments repo",
  "category": "repo",
  "status": "ready",                  // building | ready | failed
  "effective_permission": "admin",    // admin | contributor | viewer
  "folder_id": null,
  "created_at": "…",
  "updated_at": "…"

  // also: org_id, home_workspace_id, creator_user_id, description, origin_cb_id, synced,
  // category_subject, category_metadata, access_source, has_other_user_grants.
}

GET /v1/team/cbs also supports ?folder_id=, ?shared_with=, ?private_to_me=is.true, and ?include=manifest,count,….

Search memory — POST /v1/mcp/memories/search

Scope memory:read. A REST shortcut for the org-routed memory search (the routing the MCP search_memory tool uses).

// request
{ "query": "…", "top_k": 5 }   // top_k 1–20 (default 5)

// response data
{
  "scope": {
    "source": "team_facts",
    "org_id": "…", "org_name": "…",
    "workspace_id": "…", "workspace_name": "…"
  },
  "items": [
    { "id": "…", "content": "…", "score": 0.8, "source_type": "fact" }
  ]
}

Manage your Memory API keys

The control surface — mint & manage the xtk_ data-plane keys your apps use against the Memory API. Requires memory:write (it's credential management, so even the reads need write scope) plus the right role; frozen orgs are blocked.

EndpointRoleIn → out
POST /v1/developer/mem-manager/keysadmin{ name, description? } → key + secret
GET /v1/developer/mem-manager/keysmember?status=key[]
GET /v1/developer/mem-manager/keys/{hash}member— → key
PUT /v1/developer/mem-manager/keys/{hash}admin{ name, description? }key
PATCH /v1/developer/mem-manager/keys/{hash}/status/revokeadmin— → key

name is required (1–200); description optional (≤1000).

// a key
{
  "hash": "…",
  "first_n": "xtk_ab…",
  "name": "prod",
  "description": null,
  "status": "ACTIVE",            // ACTIVE | REVOKED
  "created_by_user_id": "…",
  "created_at": "…", "updated_at": "…", "revoked_at": null
}

// create wraps it and adds the one-time secret:
// { "api_key": { …the key… }, "secret_key": "xtk_…" }

Webhooks — /v1/developer/webhooks

Scope memory:write, admin on every route (the config holds a signing secret). GET / POST / PUT / POST /rotate return the config; DELETE204.

// set — POST/PUT body
{
  "url": "https://you.example.com/hook",
  "events": ["memory.learning.completed", "memory.learning.failed"],  // default: all
  "enabled": true
}

// config — response (secret revealed on create + rotate, masked on get/put)
{
  "object": "webhook",
  "url": "…",
  "events": ["…"],
  "enabled": true,
  "secret": "whsec_…",
  "created_at": "…", "updated_at": "…"
}

Usage — /v1/developer/mem-manager/usage

Scope memory:write, member. Read your Memory-API usage; from / to are YYYY-MM-DD.

EndpointQueryReturns
GET …/usage/keys/{hash}/dailyfrom, todaily metrics over the period
GET …/usage/organization/dailyfrom, todaily metrics over the period
GET …/usage/organization/totalfrom, toorg totals + per-key breakdown
GET …/usage/summarydaily=true?, by_api_key=true?rich summary (quota, storage, …)

The counters throughout are { messages_ingested, searches, requests }.