Audience: developers building Python, Node, or any other SDK client against caura.ai or a self-hosted Caura (formerly MemClaw) instance, without installing the OpenClaw plugin runtime.
Time to first tool call: ~5 minutes.
One credential model. Every API credential — tenant-scoped, agent-scoped, install, cross-tenant — uses the
mc_prefix on the wire and lives in one underlying credential table. Scope is bound to the credential row at mint time, not encoded in the prefix. Pre-existingmca_…/mci_…keys continue to authenticate via back-compat; new mints all returnmc_….
- A tenant-scoped
mc_credential — create one atcaura.ai/connect/keys(Connect › Keys in the Caura app), or self-host. - An MCP client. Examples below use
mcp(Python) andcurl. The same flow works withanthropic(Python SDK's remote-MCP integration),openai, or any client that speaks MCP streamable-http.
Every long-lived integration should bind to a named agent identity rather than calling under a tenant-scoped credential.
Provisioning is on the managed platform (caura.ai). A self-hosted core-api has no provisioning route: there, use the tenant-scoped (or standalone) key and pass an explicit agent_id on every call.
The dashboard's Connect › Keys (caura.ai/connect/keys) makes only API keys, which are tenant-scoped (optionally widened to read the org's other tenants) and carry no agent identity, so agent_id is passed on each call. Making keys of either kind needs the Manage API credentials permission; without it, ask an organization admin.
- API (for scripted provisioning, shown below):
POST /api/v1/admin/agent-keys/provisionon the managed platform (caura.ai) — atomic call that creates the Agent row eagerly so subsequent trust-elevation or fleet-assignment endpoints work immediately:
curl -X POST https://caura.ai/api/v1/admin/agent-keys/provision \
-H "X-API-Key: $MC_TENANT_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "quote-agent-na",
"label": "north-america CRM",
"initial_trust": 1,
"initial_fleet": "na-sales"
}'Response:
{
"id": "…",
"tenant_id": "…",
"agent_id": "quote-agent-na",
"raw_key": "mc_…",
"agent_row_created": true,
"created_at": "…"
}Save raw_key immediately — it's only returned once. The returned credential uses the mc_ prefix regardless of kind; the gateway derives scope (tenant / agent / cross-tenant) from the credential row, not the prefix. agent_row_created: true confirms the Agent row exists and PATCH /agents/quote-agent-na/trust will work without a synthetic first write.
Optional fields on the provision request:
initial_trust—0,1,2,3(default1).initial_fleet— fleet membership; absent = no fleet, and writes then landfleet_id: null, which is tenant-shared by design: every fleet'ssearchandrecallsee the row. Note the asymmetry before you use counts as a health check — a fleet-filteredGET /memoriesor/statsdoes not return those rows today, so a fleet-less agent's writes answer teammates' searches while being absent from their listings.display_name— human-readable name surfaced on the dashboard.
Before making real tool calls, confirm Caura resolves your credentials the way you expect:
curl https://caura.ai/api/v1/whoami \
-H "X-API-Key: $AGENT_KEY"{
"tenant_id": "your-tenant-id",
"agent_id": "quote-agent-na",
"auth_source": "gateway-header",
"via_gateway": true
}If agent_id is null or doesn't match what you provisioned, your credential isn't recognized as agent-scoped. Common causes:
- You're sending a tenant-scoped credential, not the agent-scoped one you provisioned.
- The credential was revoked or rotated.
- A proxy in front of Caura is stripping the
X-API-Keyheader.
Latency measurement: establish a warmed baseline for
POST /searchandPOST /recallunder your expected concurrency. No current public latency figure is approved; seeperformance.mdfor measurement guidance and caveats.
Caura speaks MCP streamable-http at /mcp (the trailing slash is optional; both /mcp and /mcp/ work).
Caura accepts the credential on either of these — pick whichever your SDK supports:
| Header | When to use |
|---|---|
X-API-Key: mc_… |
Canonical. Use if you control the request shape. |
Authorization: Bearer mc_… |
OAuth-style. Required by Anthropic's remote-MCP integration and other SDKs that only emit Authorization headers. |
Legacy mca_… / mci_… keys continue to authenticate on both headers via back-compat. JWTs from the dashboard are also accepted via Authorization: Bearer <jwt>; Caura distinguishes them by trying JWT decode first.
import asyncio
from mcp.client.session import ClientSession
from mcp.client.streamable_http import streamablehttp_client
async def main():
headers = {"X-API-Key": "mc_..."} # agent-scoped credential
url = "https://caura.ai/mcp/"
async with streamablehttp_client(url, headers=headers) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print([t.name for t in tools.tools])
result = await session.call_tool(
"caura_write",
{"content": "First memory from the Python harness."},
)
print(result)
asyncio.run(main())from anthropic import Anthropic
client = Anthropic()
msg = client.messages.create(
model="claude-opus-4-7",
max_tokens=1024,
messages=[{"role": "user", "content": "Save this note: pricing meets 10 May."}],
extra_body={
"mcp_servers": [
{
"type": "url",
"url": "https://caura.ai/mcp/",
"name": "caura",
"authorization_token": "mc_...", # agent-scoped credential
}
]
},
)
print(msg.content)The SDK forwards authorization_token as Authorization: Bearer mc_…. Caura recognises that shape and resolves your tenant + agent identity from the credential row.
The default trust_level=1 lets the agent write to its home fleet. Elevate when you need cross-fleet writes, keystone authoring, or deletes:
curl -X PATCH "https://caura.ai/api/v1/agents/quote-agent-na/trust?tenant_id=$TENANT_ID" \
-H "X-API-Key: $MC_TENANT_KEY" \
-H "Content-Type: application/json" \
-d '{"trust_level": 2}'Trust levels:
0— read-only.1— write to home fleet.2— cross-fleet read.3— cross-fleet write + delete + update others' memories.
If you provisioned with initial_trust, this step is already done — confirm with /whoami.
Keystones are mandatory rules that override conflicting instructions. Authoring a
scope=fleet/scope=tenant rule needs trust ≥ 2 (see above). The one tier below
that is self-authoring: scope=agent with an explicit agent_id equal to the
calling agent drops to trust ≥ 1. Omitting agent_id on a scope=agent rule
does not mean "myself" — it names no target, so it falls back to the trust ≥ 2
bar (and is rejected by shape validation regardless).
Two things can hold the floor at ≥ 2 even for a correctly-shaped self rule, so treat trust 1 as the floor for the shape rather than a guarantee for the call:
- An existing rule at the same
doc_id. The required trust is the higher of what your new body needs and what the already-stored rule needs. Overwriting ascope=fleetrule takes trust ≥ 2 no matter how you shape the replacement — this is what stops a trust-1 agent quietly replacing a fleet-wide rule with a private one. - An unverified caller identity. The self-author tier needs an agent-scoped
credential (
POST /admin/agent-keys/provisionon the managed platform), not anX-Agent-IDheader sent alongside a tenant/admin key, so that an admin-key holder can't forge a rule in another agent's name.
curl -X POST "https://caura.ai/api/v1/keystones" \
-H "X-API-Key: $MC_TENANT_KEY" \
-H "X-Agent-ID: quote-agent-na" \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "'"$TENANT_ID"'",
"doc_id": "no-secrets-in-logs",
"title": "No secrets in logs",
"content": "Never log credentials or API keys.",
"scope": "tenant",
"weight": "high"
}'doc_id is required — a stable kebab-case slug you choose
(^[a-z0-9][a-z0-9._-]{0,99}$). It's the rule's identity: re-POSTing the same
doc_id upserts (edits) that rule rather than creating a duplicate. weight is
low / med / high (stored as 25 / 50 / 100). For scope=fleet/agent
also pass fleet_id; scope=agent additionally takes the target agent_id
(omit agent_id for tenant/fleet).
TENANT_KEY=mc_... # tenant-scoped credential
AGENT_ID="quote-agent-na"
FLEET_ID="na-sales"
# 1. Provision agent + Agent row + trust + fleet in one call.
RESP=$(curl -s -X POST https://caura.ai/api/v1/admin/agent-keys/provision \
-H "X-API-Key: $TENANT_KEY" \
-H "Content-Type: application/json" \
-d "{\"agent_id\":\"$AGENT_ID\",\"initial_trust\":1,\"initial_fleet\":\"$FLEET_ID\"}")
AGENT_KEY=$(echo "$RESP" | python3 -c "import json,sys; print(json.load(sys.stdin)['raw_key'])")
# AGENT_KEY is an mc_… agent-scoped credential.
# 2. Verify.
curl -s https://caura.ai/api/v1/whoami -H "X-API-Key: $AGENT_KEY"
# 3. Use.
curl -s https://caura.ai/api/v1/memories \
-H "X-API-Key: $AGENT_KEY" \
-H "Content-Type: application/json" \
-d "{\"tenant_id\":\"$TENANT_ID\",\"agent_id\":\"$AGENT_ID\",\"fleet_id\":\"$FLEET_ID\",\"content\":\"Hello world\"}"Four steps, one round-trip per agent. The bootstrap dance from earlier integration attempts (provision → fake-write → patch trust → seed) is no longer necessary.
A write of identical content (same agent_id, same fleet_id) is retry-safe via MCP:
- First call →
201with the new memory id. - Identical retry →
200with{ "status": "duplicate", "existing_id": "…" }.
Cross-agent writes of identical content no longer collide — each agent gets its own record.
To make a retry safe, send an Idempotency-Key header (at most 255
characters) on POST /api/v1/memories, /api/v1/memories/bulk,
/api/v1/documents or /api/v1/evolve/report. A retry with the same key and a
byte-identical body replays the first response instead of running the request
again. POST /api/v1/memories and MCP caura_write also take the key as
metadata.idempotency_key; MCP caura_evolve takes it as idempotency_key.
A key belongs to the tenant, the agent the request is attributed to, and the
way it was sent (header, body or MCP), and it lasts 24 hours. Two agents that
happen to use the same key each get their own write. Reusing a key with a
different body is a 422. A retry that arrives while the first request is
still running waits for it, and gets a 409 if the first takes too long.
Every evolve report adjusts weights, even when the outcome text repeats: the same outcome reported after two different actions is two reports. A retry after a timeout is the same report, so send it with the first attempt's key. It then replays the first answer and adjusts nothing again.
Every write endpoint (POST /api/v1/memories, /api/v1/documents,
PATCH /api/v1/memories/{id}, …) responds 422 to a field it does not
declare, and names it:
{
"error": {
"code": "INVALID_ARGUMENTS",
"message": "unknown field 'tags' is not permitted on this request body (at 'tags')",
"details": { "unknown_fields": ["tags"] }
}
}This used to be a silent 201: the key was dropped and the write reported
success without it. If you are porting an integration that predates this, run
your write payloads once and fix whatever comes back 422 — those fields were
never being stored.
Search and filter bodies (/api/v1/search, /api/v1/recall,
/api/v1/documents/query, /api/v1/documents/search) still ignore unknown
fields, deliberately. See
api-surfaces.md
for why the two differ.
POST /provisionreturns the raw key once. Save it before the response goes out of scope.PATCH /agents/{id}/trustreturns 404 immediately after provisioning. This should not happen post-2026-05-13; if it does, the Agent row was not materialized atomically. CheckwhoamiandGET /api/v1/agents/{id}./mcpreturns 401 with anAuthorization: Bearer mc_…(or legacymca_…) header but works withX-API-Key. Make sure you're hitting a Caura build dated 2026-05-13 or later — earlier builds rejected non-JWT bearer tokens.- A write that used to return
201now returns422. Readerror.details.unknown_fields. The named field is not part of the request model — it was being discarded before, so the fix is to remove it or move it undermetadata, not to retry. scopeandvisibilityare different axes, andscopeitself means two things. On a read,scope=agent|fleet|allchooses how wide to look. On a write,visibility=scope_agent|scope_team|scope_orgstamps who may see the row — it is not a read breadth and passing it as one filters rather than widens. Keystone routes takescopewith a third enum,tenant|fleet|agent, soscope=allthere is not a value. And a memory's ownscopefield in a response is none of the above: it carries validity qualifiers such as role or task. Four spellings, one word; check which surface you are on before copying a value between them.- Streaming client hangs on initialize. If hitting
/mcp(no slash) caused a hang on older builds, append the trailing slash or upgrade — current builds serve both paths without redirect. caura_insightsis cut off by your client's default tool timeout. It is LLM-backed and runs roughly 7–9 s — an order of magnitude slower than the other tools — while many MCP clients default to a 5–10 s tool timeout. There is no cheap/counts-only mode and no partial result, so a client that times out loses the whole call. Raise the timeout for this tool to ~30 s. See Tool latency below.
Set your MCP client's tool timeout from the slowest tool you actually call, not from an average.
| Tool | Observed | Notes |
|---|---|---|
caura_insights |
6.8 s and 8.7 s | LLM-backed synthesis. No depth/quick mode and no partial results — a timeout loses the entire call. |
caura_write (MCP) |
0.86 s | Inline enrichment on the default write mode. |
| REST calls, warmed | 0.7–1.4 s | Storage-routed; no LLM on the request path. |
A 30 s tool timeout covers all of these with margin. A 5–10 s default — common
in MCP clients — will truncate caura_insights specifically.
Two caveats on these numbers, so they are read for what they are. They come from two runs of the parity smoke against production on 2026-08-24, not from a sustained benchmark — treat them as an order of magnitude, not an SLA. And the first call of a session is unrepresentative: that run measured a 16 s first write, which later investigation did not explain (core-api holds a min-instance floor, so it was not a scale-from-zero cold start). Warm the connection before timing anything.
caura_recall with include_brief=true adds a second LLM round-trip on top of
the search; it is not in the table because this pass did not measure it
separately. Time it yourself before choosing a timeout if you rely on it.
POST /api/v1/admin/agent-keys/provision— atomic provisioning on the managed platform (this guide).GET /api/v1/whoami— identity probe.GET /api/v1/agents/{id}?tenant_id=...— agent detail.PATCH /api/v1/agents/{id}/trust?tenant_id=...— change trust level.POST /api/v1/memories— REST write (mirrorscaura_writeover MCP).mcp://…/mcp/— streamable-http MCP endpoint.