Caura / docs
Integrations

Per-agent keys (no plugin)

Bootstrap a long-lived integration without the OpenClaw plugin — provision an agent-scoped credential, identify, and call MCP.

If you're building a Python / Node / Go integration directly against Caura — without the OpenClaw plugin runtime — bind every long-lived agent to its own agent-scoped credential rather than calling under the tenant-scoped key. Agent-scoped credentials give you trust gating, fleet membership, per-agent keystones, and a real identity in the dashboard.

Both kinds of credential use the mc_ prefix on the wire; scope (tenant vs agent) is bound at mint time on the credential row, not encoded in the prefix. (Pre-existing mca_… keys continue to authenticate via back-compat.)

The single tenant-scoped flow on the REST and MCP pages is fine for one-off scripts; the four-step flow here is for anything that ships.

1. Mint an agent-scoped credential

POST /api/v1/admin/agent-keys/provision mints an agent-scoped credential and creates the Agent row eagerly, so a follow-up PATCH /agents/{id}/trust works in the same session without a synthetic first write.

Authenticate with an org-admin session or a tenant-level API key with write capability. Default tenant keys with no explicit capability list also have the full grant. Agent-scoped and read-only keys cannot mint agent credentials.

curl -X POST "https://caura.dev/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 is only returned once. agent_row_created: true confirms the Agent row exists. initial_trust and initial_fleet apply when the Agent row is new; reissuing a key preserves an existing Agent's trust and fleet.

Request body fields:

  • agent_id (required) — stable identifier for this agent. Surfaces in /whoami, audit, and per-agent dashboards.
  • label — human-readable hint in the dashboard.
  • initial_trust — 0 read-only / 1 write to home fleet / 2 cross-fleet read / 3 cross-fleet write + delete. Default 1.
  • initial_fleet — fleet membership; omit for tenant-wide scope.
  • display_name — human-readable name surfaced on the dashboard.

2. Verify identity (/whoami)

Before any real tool call, confirm Caura resolves your credentials the way you expect (the tenant_id it returns names the key's tenant):

curl "https://caura.dev/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 you're sending a tenant-scoped credential, not an agent-scoped one — the gateway only injects X-Agent-ID on the agent-scoped path. (Both kinds share the mc_ prefix. On Connect › Keys, people who manage the organization's credentials see which is which in the Kind column. Agent keys are minted with the provisioning call in step 1, or by the OpenClaw plugin.)

3. Open an MCP session

Caura speaks MCP streamable-http at /mcp (both /mcp and /mcp/ work). The server accepts the API key on either header — use whichever your SDK supports:

HeaderWhen 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 SDKs that only emit Authorization headers.

Dashboard-issued JWTs are also accepted via Authorization: Bearer <jwt>; the server tries JWT decode first.

Python (the mcp library)

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.dev/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."},
          )
          # Always inspect both result.isError and the content envelope.
          # Gateway-side refusals (FORBIDDEN, INVALID_ARGUMENTS, NOT_FOUND, ...)
          # arrive as isError=True with a JSON {"error":{...}} body — naive
          # clients that only check content[0].text for "id" will silently
          # treat failure as success.
          if result.isError:
              print("tool refused:", result.content[0].text)
          else:
              print(result)

asyncio.run(main())

Anthropic SDK (remote-MCP)

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.dev/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 tenant + agent identity from it.

4. Elevate trust (when needed)

If you set initial_trust in step 1, skip this. Otherwise:

curl -X PATCH "https://caura.dev/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.

End-to-end bootstrap, one block

TENANT_KEY=mc_...
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.dev/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'])")

# 2. Verify.
curl -s "https://caura.dev/api/v1/whoami" -H "X-API-Key: $AGENT_KEY"

# 3. Use.
curl -s "https://caura.dev/api/v1/memories" \
-H "X-API-Key: $AGENT_KEY" \
-H "Content-Type: application/json" \
-d "{\"agent_id\":\"$AGENT_ID\",\"fleet_id\":\"$FLEET_ID\",\"content\":\"Hello world\"}"

Four steps, one round-trip per agent. No provision → fake-write → patch-trust dance.

Idempotency

A write of identical content (same agent_id, same fleet_id) is retry-safe via MCP:

  • First call → 201 with the new memory id.
  • Identical retry → 200 with { "status": "duplicate", "existing_id": "…" }.

Cross-agent writes of identical content no longer collide — each agent gets its own record.

Authoring keystones

When you want an agent to author governance rules (keystones), use caura_keystones_set over MCP:

result = await session.call_tool(
    "caura_keystones_set",
    {
        "op": "set",
        "doc_id": "no-rollback-to-cve-versions",
        "title": "Refuse rollback to CVE versions",
        "content": "Never recommend rolling back to a version listed in any CVE entry.",
        "scope": "tenant",
        "weight": "high",
    },
)

Two gotchas worth knowing before you write the call:

  • agent_id in caura_keystones_set names the TARGET agent the rule binds to, not the caller. Pass it only for scope=agent. For scope=tenant or scope=fleet, omit it — passing it returns INVALID_ARGUMENTS.
  • The companion read tool caura_keystones returns the merged rule set under the JSON key rules (not keystones).

Trust gating is dynamic: self-authoring (scope=agent + target = caller) needs trust ≥ 1; anything else (fleet, tenant, or targeting another agent) needs trust ≥ 2.

Common pitfalls

  • POST /provision returns the raw key once. Save it before the response goes out of scope.
  • PATCH /agents/{id}/trust returns 404 immediately after provisioning. Means the Agent row was not materialised atomically — should not happen on builds after 2026-05-14. Check /whoami and GET /api/v1/agents/{id}; if agent_row_created: false is in the provision response, the deployment is missing CORE_STORAGE_API_URL.
  • MCP tool result has isError=False but content[0].text is {"error": …}. Pre-2026-05-15 builds left isError at the default for gateway-side refusals. On current builds, every structured-error envelope arrives with isError=True.
  • Streaming client hangs on /mcp (no slash). Older builds redirected /mcp → /mcp/; current builds serve both paths in-process.

Reference

  • POST /api/v1/admin/agent-keys/provision — atomic provisioning (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 (mirrors caura_write over MCP).
  • mcp://…/mcp/ — streamable-http MCP endpoint.