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-Idheader 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 needsmemory:write. A read-only key is rejected from every write, on both MCP and REST. - Rate limit — one seat's throughput. Over it →
429with aRetry-Afterheader. - REST envelope — responses are wrapped:
{ "code": 0, "msg": "ok", "data": <payload> }(code0 = success). The shapes below are thedatapayload.
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 anxtk_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
| Tool | Arguments | What it returns |
|---|---|---|
search_memory | query*, memory_type, top_k, agent_brain_id, tags, match, created_after, created_before | Ranked memory items (facts / artifacts / episodes / documents) with scope + items[]. |
search_brains | query*, top_k, folder | Which agent brains are most relevant to a topic. |
search_all_brains | query*, memory_type, top_k, folder | Matching items across every brain you can access. |
recall_directives | tool, args, output, entities, task, repo, already_fired, session_id, limit | Situated lessons/procedures relevant to what you're doing. |
list_tags | agent_brain_id | The tag vocabulary + counts in a scope. |
get_artifact | artifact_id*, agent_brain_id | Full content of one artifact. |
get_artifact_lineage | artifact_id*, agent_brain_id | Every version in the chain + derived_from links. |
diff_artifact_versions | from_id*, to_id*, agent_brain_id | Unified diff between two artifact versions. |
get_brain_overview | agent_brain_id* | The compiled overview of a brain. |
list_agent_brains | created_after, created_before | Brains you can access, newest first. |
list_agent_brain_access | agent_brain_id* | Who has access to a brain and at what level. |
list_brain_folders | — | Your brain-grouping folders. |
list_workspaces | — | Workspaces you can use. |
list_orgs | — | Orgs you belong to + the default this connection acts in. |
list_teammates | — | Active members of your org (excluding you). |
list_skills | agent_brain_id, limit | Approved skills in a scope, with SKILL.md text. |
get_skill | skill_id*, agent_brain_id | One skill's full SKILL.md by id or name. |
get_skill_file | skill_id*, path*, agent_brain_id | One file from a skill's bundle, base64-inline. |
Write tools — need memory:write
| Tool | Arguments | What it does |
|---|---|---|
add_memory | user_message*, assistant_message, conversation_id, message_id, agent_brain_id | Save a conversation turn into team memory. |
import_conversation | messages*, conversation_id, title, source_platform, agent_brain_id, namespace | Import a whole conversation in one call. |
save_artifact | name*, content, artifact_type, agent_brain_id, parent_id, rationale, tags, files, entrypoint | Save a document/spec as an artifact (versioned). |
ingest_document_from_url | url*, name, agent_brain_id | Ingest a public URL as a searchable document. |
tag_artifact | artifact_id*, add_tags, remove_tags, agent_brain_id | Add/remove tags on an artifact (no new version). |
tag_document | document_id*, add_tags, remove_tags, agent_brain_id | Add/remove tags on an ingested document. |
delete_artifact | artifact_id*, agent_brain_id | Delete an artifact + its whole version history. |
delete_fact | fact_id*, agent_brain_id | Retract a fact so it stops surfacing. |
delete_episode | episode_id*, agent_brain_id | Delete an episode. |
copy_memory_to_brain | item_ids*, agent_brain_id* | Copy existing memory into a brain. |
create_skill | name*, trigger*, skill_md*, scripts, assets, summary, agent_brain_id | Save an executable SKILL (Agent-Skills format). |
add_skill_to_brain | skill_id*, agent_brain_id*, source_agent_brain_id, refresh | Copy a skill into a brain. |
delete_skill | skill_id*, agent_brain_id | Delete a skill. |
create_agent_brain | name*, description, workspace_id, category | Create an agent brain. |
refresh_brain_overview | agent_brain_id* | Regenerate a brain's overview now. |
share_agent_brain | agent_brain_id*, teammate_user_id*, permission | Share a brain / change a teammate's access. |
create_workspace | name* | Create a workspace. |
create_brain_folder | name* | Create a folder for grouping brains. |
move_brain_to_folder | agent_brain*, folder* | File a brain into a folder. |
remove_brain_from_folder | agent_brain* | Take a brain out of its folder. |
REST endpoints
- Base
https://api.memhub.xtrace.ai/v1 - Headers
Authorization: Bearer mhk_…, plusX-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 thedata.
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.
| Endpoint | Scope | In → out |
|---|---|---|
GET /v1/team/facts | read | query filters → fact[] |
POST /v1/team/facts/search | read | search 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/artifacts | read | query filters → artifact[] |
POST /v1/team/artifacts/search | read | search body → { hits, scopes } |
PATCH /v1/team/artifacts?logical_id=eq.<id> | write | patch body → artifact |
DELETE /v1/team/artifacts?logical_id=eq.<id> | write | — → { deleted, scope } |
GET /v1/team/cbs | read | query filters → brain[] |
POST /v1/team/cbs | write | create body → brain |
POST /v1/team/cbs/{cb_id}/search | read | search 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.
| Endpoint | Role | In → out |
|---|---|---|
POST /v1/developer/mem-manager/keys | admin | { name, description? } → key + secret |
GET /v1/developer/mem-manager/keys | member | ?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/revoke | admin | — → 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; DELETE → 204.
// 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.
| Endpoint | Query | Returns |
|---|---|---|
GET …/usage/keys/{hash}/daily | from, to | daily metrics over the period |
GET …/usage/organization/daily | from, to | daily metrics over the period |
GET …/usage/organization/total | from, to | org totals + per-key breakdown |
GET …/usage/summary | daily=true?, by_api_key=true? | rich summary (quota, storage, …) |
The counters throughout are { messages_ingested, searches, requests }.