API reference
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./mcpand/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.
Health and readiness
Section titled “Health and readiness”curl http://localhost:4000/healthcurl 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.
OAuth discovery
Section titled “OAuth discovery”GET /.well-known/oauth-protected-resourceGET /.well-known/oauth-protected-resource/mcpGET /.well-known/oauth-protected-resource/mcp/<capsule-id>GET /.well-known/oauth-authorization-serverGET /.well-known/openid-configurationPOST /oauth/registerGET /oauth/authorizePOST /oauth/tokenPOST /oauth/revokeMCP 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"Dashboard auth
Section titled “Dashboard auth”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.
Capsule create
Section titled “Capsule create”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..." } } ]}Knowledge
Section titled “Knowledge”POST /v1/capsules/:id/knowledge— write text.POST /v1/capsules/:id/knowledge/upload— multipart upload.GET /v1/capsules/:id/knowledge— list with content inline.
Connections
Section titled “Connections”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/connectionsGET /v1/capsules/:id/sharesPOST /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/:code—active,paused, orrevoked.DELETE /v1/shares/:code— legacy delete path.
Agent enrollments
Section titled “Agent enrollments”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 withAuthorization: Bearer <enrollment-token>. Pending polls do not return MCP connection details.GET /v1/agent-enrollments— dashboard/admin queue.POST /v1/agent-enrollments/:id/approvePOST /v1/agent-enrollments/:id/reject
Enrollment requests create grant records. Operators can act on those grants directly:
GET /v1/grantsPOST /v1/grants/:id/approve— approves OAuth and enrollment grants. Approved OAuth clients retry authorization to receive the code.POST /v1/grants/:id/denyPOST /v1/grants/:id/expirePOST /v1/grants/:id/revoke— revokes an approved grant and its MCP connection.GET /v1/access-profilesGET /v1/approval-policyPATCH /v1/approval-policyGET /v1/oauth-clients
CLI equivalents:
capsol agent enroll --registry http://localhost:4000 --capsule <id> --client-id <stable-agent-id>capsol enroll approve <code> --registry http://localhost:4000capsol grants list --registry http://localhost:4000capsol grants approve <grant-id> --registry http://localhost:4000 --scopes capsule:readcapsol grants deny <grant-id> --registry http://localhost:4000Registry settings
Section titled “Registry settings”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.
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:
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" }'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:
| Case | Status |
|---|---|
Invalid Origin header | 403 |
Missing Accept: application/json, text/event-stream | 406 |
Non-JSON Content-Type | 415 |
| Malformed JSON-RPC request | 400 |
| JSON-RPC response-only payload posted to stateless MCP | 400 |
| Unknown client notification | 400 |
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.
Proposals and approvals
Section titled “Proposals and approvals”GET /v1/proposals admin — capsol_manage activity feed (?status= filter)POST /v1/proposals/:id/approve admin — apply a queued proposalPOST /v1/proposals/:id/deny admin — refuse a queued proposalGET /v1/approvals/pending admin or supervisor — pending enrollments + proposalsPOST /v1/approvals/:id/approve admin or supervisor — id is an enrollment or prop_ idPOST /v1/approvals/:id/deny admin or supervisorPOST /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.
Signal stream and large downloads (0.18)
Section titled “Signal stream and large downloads (0.18)”GET /mcp[/:capsuleId] Accept: text/event-stream + Bearer — standalone signal notification stream (JSON-RPC notifications/message, logger "capsol.signal"); plain GET keeps answering 405PATCH /v1/capsules/:id admin — description, uri label, max_signal_ttlGET /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
Section titled “Rate limits”Rate limits are disabled by default for local/self-hosted startup. Enable them in Settings or with CAPSOL_RATELIMIT_ENABLED=true.
| Route | Enabled limit | Key |
|---|---|---|
POST /api/capsules | 5/hour | client IP |
/v1/* | 600/min | authorization |
/mcp/* | 600/min | OAuth/enrollment credential or failed-auth IP |
GET /v1/agent-enrollments/:id | 10/min | client IP |
POST /oauth/revoke | 30/min | client IP |
Error responses
Section titled “Error responses”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_code | Status | Meaning | Recovery |
|---|---|---|---|
invalid_token | 401 | Credential not recognized | Re-run OAuth discovery/DCR, or enroll via POST /v1/agent-enrollments |
token_expired | 401 | Access token past its expiry | Use the refresh_token grant, or re-authorize |
token_revoked | 401 | Credential revoked (rotation or /oauth/revoke) | Request access again via OAuth or enrollment |
grant_revoked | 401 | The grant behind the connection was denied or cancelled by the operator | File a new access request and wait for approval |
connection_paused | 401 | Operator paused the connection; credentials stay valid | Wait for reactivation or contact the operator |
unknown_capsule | 404 | No capsule with that id | Verify the capsule id with the operator — URLs are addresses, not credentials |
csrf_required | 403 | Cookie-auth state change without a valid X-Capsol-CSRF | GET /api/auth, then resend with the returned token |
rate_limited | 429 | Bucket exhausted (bucket names it) | Wait for the Retry-After interval |
payload_too_large | 413 | Body over the limit; includes limit_bytes and actual_bytes | Split content, or use POST /v1/capsules/:id/knowledge/upload for artifacts |
invalid_uri | 400 | Malformed capsule URI (traversal, unknown scheme, null bytes) | Use scheme://path, e.g. docs://readme |
secret_key_not_persistent | 400 | Encrypted setting rejected: registry secret key is ephemeral | Set 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.