Skip to content

API reference

$ api · 8 min read · updated 2026-05-05

Surfaces:

  • /health — unauthenticated liveness.
  • /ready — readiness checks for data dir, auth, public/docs URL, OIDC, OAuth DCR/CIMD, schemes, and crypto secret.
  • /markdown/<doc-slug>.md — raw Markdown export for each docs page.
  • /api/* — dashboard login, anonymous create, dashboard activity.
  • /v1/* — admin REST API plus unauthenticated enrollment creation/polling.
  • /mcp and /mcp/:capsuleId — Streamable HTTP MCP with OAuth auth; approved agent-enrollment credentials are supported for autonomous agents.

Set CAPSOL_DOCS_URL when docs are hosted somewhere other than /docs on the registry origin. Local development defaults agent metadata to http://localhost:4321.

Terminal window
curl http://localhost:4000/health
curl http://localhost:4000/ready
{ "status": "ok", "version": "0.15.0", "uptime": 42.1 }

/ready returns redacted configuration status, including public_url, docs_url, crypto_secret_ready, OIDC readiness, DCR/CIMD capability, configured scheme count, and the data directory path.

GET /.well-known/oauth-protected-resource
GET /.well-known/oauth-protected-resource/mcp
GET /.well-known/oauth-protected-resource/mcp/<capsule-id>
GET /.well-known/oauth-authorization-server
GET /.well-known/openid-configuration
POST /oauth/register
GET /oauth/authorize
POST /oauth/token
POST /oauth/revoke

MCP clients should use the discovery documents and Dynamic Client Registration. For a capsule URL, the protected-resource document is /.well-known/oauth-protected-resource/mcp/<capsule-id> and its resource is exactly /mcp/<capsule-id>. The registry-level /mcp resource is only unambiguous when exactly one capsule exists; otherwise the operator chooses a capsule during authorization.

Access tokens issued at /oauth/token expire after 30 days by default (CAPSOL_TOKEN_TTL_SECONDS overrides). The response includes a refresh_token; the refresh_token grant rotates the access token. Rotation is additive by default — the previous access token stays valid until its recorded expiry — and immediate under CAPSOL_ROTATE_ON_REFRESH=strict. POST /oauth/revoke (RFC 7009) accepts either token and revokes both; it always returns 200, so it cannot be used to probe token existence.

Authorization server metadata advertises authorization-code OAuth and PKCE S256 only. plain PKCE is not supported. Dynamic Client Registration and CIMD (client_id_metadata_document_supported) are advertised from the current dashboard settings. /.well-known/openid-configuration returns the same OAuth endpoints for clients and debuggers that probe OIDC discovery first.

Unauthenticated MCP requests return a scoped challenge:

WWW-Authenticate: Bearer realm="capsol", resource_metadata="https://host/.well-known/oauth-protected-resource/mcp/<capsule-id>", scope="capsule:read capsule:append capsule:write signal:send"
Terminal window
curl -X POST http://localhost:4000/api/auth \
-H "Content-Type: application/json" \
-d '{ "key": "cap_live_..." }'

Sets an httpOnly, SameSite=Strict capsol_admin cookie plus a JS-readable capsol_csrf cookie. GET /api/auth reports { authenticated, csrf } for an existing session. Every cookie-authenticated POST/PATCH/DELETE must echo the CSRF token in the X-Capsol-CSRF header (the dashboard does this automatically); requests authenticated purely with a bearer key are exempt. POST /api/auth/logout clears all session cookies.

Terminal window
curl -X POST http://localhost:4000/v1/capsules \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "team-project", "description": "shared context" }'

Response includes stable MCP URLs and default connection records. Manual bearer credentials are disabled by default and are not returned.

{
"id": "9f7c...",
"name": "team-project",
"mcp_url": "http://localhost:4000/mcp/9f7c...",
"shares": [
{
"connection_id": "csl_abc...",
"role": "writer",
"scopes": ["capsule:read", "capsule:append", "capsule:write", "signal:send"],
"mcp_url": "http://localhost:4000/mcp/9f7c...",
"authorization": "OAuth",
"auth_modes": ["oauth"],
"oauth": {
"protected_resource": "http://localhost:4000/.well-known/oauth-protected-resource/mcp/9f7c...",
"registration_endpoint": "http://localhost:4000/oauth/register",
"resource": "http://localhost:4000/mcp/9f7c..."
}
}
]
}
  • POST /v1/capsules/:id/knowledge — write text.
  • POST /v1/capsules/:id/knowledge/upload — multipart upload.
  • GET /v1/capsules/:id/knowledge — list with content inline.
Terminal window
curl -X POST http://localhost:4000/v1/capsules/$ID/shares \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"role": "reader",
"label": "cursor-docs-reader",
"client_type": "Cursor",
"allowed_schemes": ["docs"],
"allowed_uris": ["docs://readme"]
}'
  • GET /v1/connections
  • GET /v1/capsules/:id/shares
  • POST /v1/capsules/:id/shares — creates an approved dashboard grant and connection record.
  • POST /v1/shares/:code/token — legacy manual bearer rotation; disabled unless legacy bearer mode is explicitly enabled.
  • PATCH /v1/shares/:codeactive, paused, or revoked.
  • DELETE /v1/shares/:code — legacy delete path.
Terminal window
curl -X POST http://localhost:4000/v1/agent-enrollments \
-H "Content-Type: application/json" \
-d '{
"client_id": "openclaw-worker-1",
"capsule_id": "<capsule-id>",
"agent_label": "OpenClaw worker 1",
"requested_role": "appender"
}'
  • POST /v1/agent-enrollments — create idempotent pending request.
  • GET /v1/agent-enrollments/:id — poll with Authorization: Bearer <enrollment-token>. Pending polls do not return MCP connection details.
  • GET /v1/agent-enrollments — dashboard/admin queue.
  • POST /v1/agent-enrollments/:id/approve
  • POST /v1/agent-enrollments/:id/reject

Enrollment requests create grant records. Operators can act on those grants directly:

  • GET /v1/grants
  • POST /v1/grants/:id/approve — approves OAuth and enrollment grants. Approved OAuth clients retry authorization to receive the code.
  • POST /v1/grants/:id/deny
  • POST /v1/grants/:id/expire
  • POST /v1/grants/:id/revoke — revokes an approved grant and its MCP connection.
  • GET /v1/access-profiles
  • GET /v1/approval-policy
  • PATCH /v1/approval-policy
  • GET /v1/oauth-clients

CLI equivalents:

Terminal window
capsol agent enroll --registry http://localhost:4000 --capsule <id> --client-id <stable-agent-id>
capsol enroll approve <code> --registry http://localhost:4000
capsol grants list --registry http://localhost:4000
capsol grants approve <grant-id> --registry http://localhost:4000 --scopes capsule:read
capsol grants deny <grant-id> --registry http://localhost:4000

GET /v1/settings returns redacted dashboard settings. OIDC client secrets and SMTP URLs are never returned; the response only includes client_secret_set and smtp_url_set.

Terminal window
curl -X PATCH http://localhost:4000/v1/settings \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"oidc": {
"enabled": true,
"issuer": "https://issuer.example.com",
"client_id": "capsol",
"client_secret": "write-only",
"allowed_domains": ["example.com"]
},
"smtp": {
"enabled": true,
"smtp_url": "smtps://user:pass@smtp.example.com:465",
"from": "capsol@example.com"
},
"oauth": {
"dcr_enabled": true,
"allowed_native_schemes": ["cursor"],
"allowed_redirect_hosts": ["chatgpt.com", "client.example.com"],
"require_client_secret_for_dcr": false,
"cimd_enabled": true,
"cimd_allow_localhost": false
}
}'

POST /v1/settings/smtp/test sends a test email with the saved SMTP configuration:

Terminal window
curl -X POST http://localhost:4000/v1/settings/smtp/test \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{ "to": "operator@example.com" }'
Terminal window
URL='http://localhost:4000/mcp/<capsule-id>'
curl -s -X POST "$URL" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Unauthenticated MCP returns 401 and WWW-Authenticate. Legacy ?token=... URLs return a migration error.

MCP POST requests are validated before reaching tool handlers:

CaseStatus
Invalid Origin header403
Missing Accept: application/json, text/event-stream406
Non-JSON Content-Type415
Malformed JSON-RPC request400
JSON-RPC response-only payload posted to stateless MCP400
Unknown client notification400

This keeps MCP debugger/readiness probes from being accepted as 202 notifications when they should fail fast.

Every tool response includes fresh state, structured identity, and signals.

GET /v1/proposals admin — capsol_manage activity feed (?status= filter)
POST /v1/proposals/:id/approve admin — apply a queued proposal
POST /v1/proposals/:id/deny admin — refuse a queued proposal
GET /v1/approvals/pending admin or supervisor — pending enrollments + proposals
POST /v1/approvals/:id/approve admin or supervisor — id is an enrollment or prop_ id
POST /v1/approvals/:id/deny admin or supervisor
POST /v1/access-profiles admin — upsert a profile (incl. auto_approve)

The /v1/approvals/* routes accept either an operator session or the designated supervisor credential (registry:approve scope; supervisor_principal_id must match when set). Agent supervisors receive 403 escalation_forbidden for any decision that would grant registry:manage or registry:approve.

GET /mcp[/:capsuleId] Accept: text/event-stream + Bearer — standalone signal
notification stream (JSON-RPC notifications/message,
logger "capsol.signal"); plain GET keeps answering 405
PATCH /v1/capsules/:id admin — description, uri label, max_signal_ttl
GET /v1/capsules/:id/knowledge/download supports Range (206 + Content-Range; 416 out of bounds)

Search over MCP is BM25-ranked with limit (≤50) and cursor pagination and a 2s budget (truncated: true on partial results). capsol_write accepts entries=[...] for batches of up to 20 with per-entry results.

Rate limits are disabled by default for local/self-hosted startup. Enable them in Settings or with CAPSOL_RATELIMIT_ENABLED=true.

RouteEnabled limitKey
POST /api/capsules5/hourclient IP
/v1/*600/minauthorization
/mcp/*600/minOAuth/enrollment credential or failed-auth IP
GET /v1/agent-enrollments/:id10/minclient IP
POST /oauth/revoke30/minclient IP

Failed REST and MCP-transport requests return a structured envelope:

{
"error": "This MCP credential has expired.",
"error_code": "token_expired",
"recovery": "Use the OAuth refresh_token grant at /oauth/token to rotate the access token, or re-authorize."
}
error_codeStatusMeaningRecovery
invalid_token401Credential not recognizedRe-run OAuth discovery/DCR, or enroll via POST /v1/agent-enrollments
token_expired401Access token past its expiryUse the refresh_token grant, or re-authorize
token_revoked401Credential revoked (rotation or /oauth/revoke)Request access again via OAuth or enrollment
grant_revoked401The grant behind the connection was denied or cancelled by the operatorFile a new access request and wait for approval
connection_paused401Operator paused the connection; credentials stay validWait for reactivation or contact the operator
unknown_capsule404No capsule with that idVerify the capsule id with the operator — URLs are addresses, not credentials
csrf_required403Cookie-auth state change without a valid X-Capsol-CSRFGET /api/auth, then resend with the returned token
rate_limited429Bucket exhausted (bucket names it)Wait for the Retry-After interval
payload_too_large413Body over the limit; includes limit_bytes and actual_bytesSplit content, or use POST /v1/capsules/:id/knowledge/upload for artifacts
invalid_uri400Malformed capsule URI (traversal, unknown scheme, null bytes)Use scheme://path, e.g. docs://readme
secret_key_not_persistent400Encrypted setting rejected: registry secret key is ephemeralSet CAPSOL_SECRET_KEY (e.g. openssl rand -base64 48), restart, retry

MCP tool results report errors inside the tool payload instead (status: "error" with code and hint); version_conflict keeps its existing shape there, including expected_version and current_version.