Skip to content

API reference

Scope: the routes whose behaviour needs explaining — auth shape, permissions, the edge cases. This page is not a mirror of the route table: the complete, always-current list is generated by FastAPI at /openapi.json — served by the backend (http://127.0.0.1:8102/openapi.json), and readable as Swagger UI at /docs (OpenAPI 3.1; 566 operations). Those paths belong to FastAPI, not to the app, and the app's origin does not serve them: the Vite dev server does not proxy them, so /docs on the frontend origin is the SPA's index (which answers 200 — check the content type, not the status).

Swagger UI's assets are vendored (backend/app/static/swagger-ui/, from swagger-ui-dist — refresh with backend/scripts/vendor_swagger_ui.sh), because the page must not need a CDN: on a network that cannot reach jsdelivr it renders blank with SwaggerUIBundle is not defined. ReDoc is off for the same reason — its bundle would be a second CDN download.

They are unauthenticated: the API's shape is public unless a deployment turns it off. The calls are not — every one is authorised by the API key's role, which is the only boundary that is actually enforced. (The skill's "use scripts/cli.py, no ad-hoc HTTP" rule is about validated inputs, -y confirmations and dry runs, not about access: a key with all can already call anything.) A hand-kept table of those is the same kind of object as the data-models.md that mirrored 88 tables by hand — and it drifted 93 routes the same way.

Agents read the generated document through the skill, not through this page: openkms api spec --path /api/<area> (see openkms-skill/references/REFERENCE.md).

backend/scripts/check_api_doc.py (run by backend/check.sh) reads FastAPI's own route table and fails if a route documented here does not exist — a documented route that is not served is a lie. It does not require this page to describe every route, and prints the count instead.

Every route described here is served by the backend. Browser-facing routes live under /api/...; routes that return provider secrets for openkms-cli, qa-agent, and workers live under /internal-api/... and require an internal service JWT (sub=local-cli or OIDC azp on OPENKMS_INTERNAL_SERVICE_CLIENT_IDS). The unauthenticated routes are explicitly marked.

For per-feature context (when an endpoint is used, what it returns), see the matching feature page in the Features index.

Localization

Clients may send Accept-Language (the SPA sends en or zh-CN). Many auth, search, and document responses use structured JSON for detail: { "code": "<STABLE_CODE>", "message": "<localized human text>" } instead of a plain string (some paths may still return a string). Stable code values are defined with English and Chinese templates in backend/app/i18n/catalog.py.

Auth, sessions, system

Method Path Description
GET /login OIDC mode: redirect to IdP. Local mode: redirect to frontend /login
GET /login/oauth2/code/oidc OAuth2 callback (backend confidential client; register on IdP)
GET /login/oauth2/code/keycloak Same as above (legacy callback path)
GET /api/auth/public-config No auth: auth_mode, allow_signup only
GET /internal-api/models/document-parse-defaults Internal service client only; query model_name optional — named model with document-parse capability or default; JSON base_url, model_name, api_key for openkms-cli
GET /internal-api/models/config-by-name Internal service client only; query model_name (required), api_kind (default chat-completions); JSON base_url, model_name, api_key for openkms-cli (e.g. pipeline metadata extraction)
GET /internal-api/models/llm-defaults Internal service client only; JSON base_url, model_name, api_key for qa-agent (default chat-completions model; same as embedded wiki agent)
GET /internal-api/models/kb-embedding-credentials Internal service client only; query knowledge_base_id (required); JSON base_url, model_name, api_key for openkms-cli kb-index (no KB ACL)
GET /internal-api/documents/{id} Internal service client only; read document (incl. metadata) for pipeline
GET /internal-api/documents/{id}/metadata-needs-extraction Internal service client only; { needs_extraction } — true when channel schema fields are all empty
PUT /internal-api/documents/{id}/markdown Internal service client only; sync parsed markdown during pipeline (no channel ACL)
PUT /internal-api/documents/{id}/metadata Internal service client only; merge extracted metadata during pipeline (no channel ACL)
POST /internal-api/documents/{id}/versions Internal service client only; snapshot markdown + metadata after pipeline (no channel ACL)
GET /internal-api/knowledge-bases/{id} Internal service client only; read KB config for kb-index (no KB ACL)
GET /internal-api/knowledge-bases/{id}/documents Internal service client only; list linked documents for kb-index
GET /internal-api/knowledge-bases/{id}/wiki-pages-for-index Internal service client only; paginated wiki pages with body for kb-index; optional wiki_space_id filter
GET /internal-api/knowledge-bases/{id}/faqs Internal service client only; paginated FAQs for kb-index
DELETE /internal-api/knowledge-bases/{id}/chunks Internal service client only; clear all chunks before full re-index
DELETE /internal-api/knowledge-bases/{id}/wiki-spaces/{wiki_space_id}/chunks Internal service client only; delete chunks for pages in one linked wiki space
POST /internal-api/knowledge-bases/{id}/chunks/batch Internal service client only; bulk insert chunks with embeddings
PUT /internal-api/knowledge-bases/{id}/faqs/batch-embeddings Internal service client only; bulk update FAQ embeddings
GET /api/public/system No auth: { "system_name" } trimmed from DB (may be ""; SPA shows openKMS when empty after load)
GET /api/system/settings Authenticated console:settings (or admin): system_name, default_timezone, api_base_url_note
PUT /api/system/settings Authenticated console:settings (or admin): update system-wide display settings
POST /api/auth/register Local mode only: create user, returns JWT + user
POST /api/auth/login Local mode only: body { "login", "password" } — login is username or email; returns JWT + user
GET /api/auth/me Current user from Bearer, session, (local) CLI Basic, or personal API key (okms.*); includes permissions (resolved keys) and optional ui_locale (en | zh-CN) from user_preferences
PATCH /api/auth/me Authenticated user: body { "ui_locale": "en" \| "zh-CN" } — save SPA display language to user_preferences (keyed by JWT sub; local and OIDC)
POST /api/auth/api-keys Authenticated user: create personal API key; response includes plaintext token once
GET /api/auth/api-keys List caller's keys (metadata only; query include_revoked=true optional)
DELETE /api/auth/api-keys/{id} Revoke caller's key (soft)
GET /api/auth/permission-catalog Authenticated: list of permission entries (key, label, description, frontend_route_patterns, backend_api_patterns) for the Console matrix, SPA route gate, and optional strict API enforcement; optional in-process TTL cache (OPENKMS_PERMISSION_CATALOG_CACHE_SECONDS, default 5; 0 disables), cleared when admins mutate security_permissions
GET /api/admin/security-roles console:permissions: roles and permission keys (includes all); is_system_role true only for admin (cannot delete)
POST /api/admin/security-roles console:permissions: create role; reserved names admin / member rejected
DELETE /api/admin/security-roles/{role_id} console:permissions: delete role (admin role rejected)
PUT /api/admin/security-roles/{role_id}/permissions console:permissions: replace keys (each must exist in security_permissions); cannot drop sole all without adding another permission first (see Permissions page)
GET /api/admin/health-status console:access: dependency checks (API, database, object storage, job queue, Ontology Function Service /health, Langfuse public health when LANGFUSE_BASE_URL is set and LANGFUSE_HEALTHCHECK is true); probes registered data sources when caller has console:data_sources
GET /api/admin/permission-reference console:permissions: frontend feature path patterns + full OpenAPI operation list (method, path, summary, tags) to help configure security_permissions
GET /api/admin/security-permissions console:permissions: paginated catalog (items, total, limit, offset; query search, category)
GET /api/admin/security-permissions/keys console:permissions: all catalog keys (for onboarding / hint diff)
POST /api/admin/security-permissions console:permissions: create catalog row
PATCH /api/admin/security-permissions/{id} console:permissions: update label, description, patterns, sort_order (built-in all row rejected)
DELETE /api/admin/security-permissions/{id} console:permissions: delete row (all and keys still assigned to roles are rejected)
GET/POST /api/admin/groups console:groups: paginated list (items, total, limit, offset; query search) or create group (member_count, shared_resource_count on each item)
GET/PATCH/DELETE /api/admin/groups/{id} console:groups: get/update/delete group (includes counts)
GET /api/admin/groups/{id}/member-subjects console:groups: all member subject ids (for full membership state)
GET/PUT /api/admin/groups/{id}/members console:groups: paginated member list (limit, offset) or replace all members (subjects)
GET /api/admin/groups/{id}/shared-resources console:groups: paginated ACL grants for this group (items, total, limit, offset)
GET/PUT /api/admin/groups/{id}/scopes Deprecated — PUT returns 410; use resource ACL sharing
GET /api/admin/resource-acl/issues console:groups: ACL issue summary (issue_count, by_issue) when called without query params
GET /api/admin/resource-acl/issues?issue={code}&limit=&offset= console:groups: paginated items for one issue code (others_manage, others_write, unknown_group, empty_group, unknown_owner, missing_owner, owner_no_permissions, owner_no_manage, implicit_others, others_read); response includes total, limit, offset, refreshed by_issue
GET/PUT /api/admin/resource-acl/{resource_type}/{resource_id} console:groups: read/replace sharing on any resource without requiring data read/manage on that resource (Console audit)
GET /api/admin/resource-acl/{resource_type}/{resource_id}/owner-candidates console:groups: local auth user list for owner picker in Console audit
GET/PUT /api/resource-acl/{resource_type}/{resource_id} Authenticated: get/replace sharing grants (r/w/m) on a securable resource. Response includes created_by (document/article channels, wiki spaces, knowledge bases), owner_subject, owner_label; when no owner ACL exists but created_by is set, GET returns a default owner grant (rwm) for the creator. PUT preserves owner when omitted; if none exists, defaults owner to created_by when set.
GET /api/resource-acl/{resource_type}/{resource_id}/owner-candidates Requires manage on the resource. Returns {subject, label} for owner assignment: local auth lists DB users; OIDC lists API-key identities, mapped local users, and access-group member subjects.
POST /api/auth/logout Clear server session
POST /sync-session Sync frontend JWT to backend session (Bearer required)
POST /clear-session Clear backend session (called before logout)
GET /logout Clear session; OIDC: redirect to IdP logout; local: redirect to frontend
GET /api/home/hub Any authenticated user. Returns site_summary: { document_count, kb_count, wiki_page_count, article_count } for the platform Home welcome panel. (Phase 2 may add recent_activity from audit logs.)
GET /api/search Authenticated unified metadata search: query q, types (all or comma-list: documents, articles, wiki_spaces, knowledge_bases), optional document_channel_id, article_channel_id, updated_after / updated_before (ISO 8601), limit (1–100, default 30). Returns sections with items (id, name, title, kind, url_path, channel_id, channel_name, updated_at) and total per type; types the user cannot read are empty; 403 if none of the requested types are allowed; 404 if a channel id is unknown. Scoped like list APIs (documents, articles, wiki spaces, KB visibility). wiki_spaces items use url_path /wikis/{id}/pages/graph.
HEAD /api/search Same auth / permission overlap check as GET; no JSON body
GET /api/providers/{id}/models Authenticated: list models registered under this provider
GET /api/admin/users console:users: auth mode, IdP notice, users (local accounts or OIDC sign-in directory)
POST /api/admin/users console:users, local only: create user (email, username, password, is_admin)
PATCH /api/admin/users/{id} console:users, local only: set is_admin (syncs security roles)
DELETE /api/admin/users/{id} console:users, local only: delete user

Documents

The bundled openkms-skill CLI wraps lifecycle and relationships the same way as the document detail page: documents lifecycle patch, documents relationships list|create|delete (see openkms-skill/references/REFERENCE.md).

Method Path Description
GET /api/document-channels List document channels (tree). Query: limit (default 200, max 500), offset — paginates root channels; each page includes full subtrees. Response: { items, total, limit, offset }
GET /api/document-channels/{id} Get channel by ID (includes label_config, extraction_schema)
POST /api/document-channels Create channel
PUT /api/document-channels/{id} Update channel (name, description, parent_id, pipeline_id, auto_process, extraction_model_id, extraction_schema, label_config, object_type_extraction_max_instances)
POST /api/document-channels/{id}/reorder Move channel up or down among siblings (body: { direction: "up" \| "down" })
POST /api/document-channels/merge Merge source channel into target (move documents, delete source; optional include_descendants)
DELETE /api/document-channels/{id} Delete channel (fails if has documents or sub-channels)
POST /api/documents/upload Multipart: file, channel_id. Stores original to S3. XLSX: sheet preview + markdown in-process, completed or failed (no parse job). XMIND: outline markdown from archive content.json / content.xml, completed or failed (no parse job). Other types (PDF, images, DOCX, PPTX, EPUB, …): uploaded; if channel auto_process and pipeline, enqueues run_pipeline (not for XLSX/XMIND)
GET /api/documents?channel_id=&search=&status=&applicable=&offset=&limit= List documents; channel_id optional (all if omitted, descendants included when filtering by channel); search filters by name; status filters by processing status (uploaded, pending, running, completed, failed); applicable (true / false) filters by lifecycle-derived applicability (is_current_for_rag); offset/limit for pagination. Returns lightweight list items only (no markdown, parsing_result, or metadata)
GET /api/documents/stats Get document counts (e.g. total) for index page
GET /api/documents/{id} Get document by ID
PUT /api/documents/{id} Update document info (name, channel_id)
GET /api/documents/{id}/parsing Get parsing result (result.json)
GET /api/documents/{id}/files/{file_hash}/{path} Redirect to presigned S3 URL via frontend proxy; ?url_only=true returns { "url": "..." } JSON (for authenticated downloads without following redirects in fetch)
DELETE /api/documents/{id} Delete document and its storage files
POST /api/documents/{id}/reset-status Reset processing status to uploaded when status is pending, failed, completed, or running (rejects if active jobs exist)
PUT /api/documents/{id}/metadata Update document metadata (partial merge)
PUT /api/documents/{id}/markdown Update document markdown and rebuild page index in S3
POST /api/documents/{id}/restore-markdown Restore markdown from S3 {file_hash}/markdown.md and rebuild page index
POST /api/documents/{id}/rebuild-page-index Rebuild page index from current markdown (DB or S3) and store in S3
GET /api/documents/{id}/page-index Get PageIndex tree structure (built during pipeline or after markdown edit, served from S3)
GET /api/documents/{id}/section Get markdown section by line range (start_line, end_line; 1-based inclusive; for QA agent Page Index skill)
POST /api/documents/{id}/versions Create explicit version (snapshot of current markdown + metadata; body: optional tag / note)
GET /api/documents/{id}/versions List document versions (metadata only; no full markdown in list)
GET /api/documents/{id}/versions/{version_id} Get full version snapshot (markdown + metadata)
POST /api/documents/{id}/versions/{version_id}/restore Restore working copy from version (body: optional save_current_as_version, tag, note); rebuilds page index
POST /api/documents/{id}/extract-metadata Extract metadata from markdown using channel's LLM
PATCH /api/documents/{id}/lifecycle Update policy lifecycle fields (series_id, effective_from, effective_to, lifecycle_status)
GET /api/documents/{id}/relationships List outgoing and incoming document relationships
POST /api/documents/{id}/relationships Create outgoing edge (target_document_id, relation_type, optional note)
DELETE /api/documents/{id}/relationships/{relationship_id} Delete an outgoing relationship (source must be this document)
GET /api/documents/{id}/export Export all stored parsing files (original, result.json, markdown.md, page_index.json, extracted_metadata.json, layout images, block images, markdown_out) as a zip archive. Response: Content-Type: application/zip with Content-Disposition: attachment
POST /api/documents/{id}/import Import a previously exported parsing zip (archive multipart field). Restores S3 files and document state (markdown, parsing_result, status→completed); rebuilds page index
POST /api/documents/{id}/import-chunk Chunked import of a parsing zip. Fields: archive (chunk bytes), chunk_index (0-based), total_chunks. Chunks stored under $TMPDIR/openkms-chunks/{id}/; when all arrive, reassembled and processed like /import. Frontend auto-splits files > 10 MB into 10 MB chunks
POST /api/documents/upload-chunk Chunked document upload. Fields: file_chunk, chunk_index, total_chunks, channel_id, filename. Reassembles then processes like /upload. Auto-split > 10 MB on frontend

Pipelines, jobs, providers, models

Method Path Description
GET /api/pipelines List pipeline configurations (limit, offset; default limit 50, max 200)
GET /api/pipelines/template-variables List available command template variables
POST /api/pipelines Create pipeline
GET /api/pipelines/{id} Get pipeline detail
PUT /api/pipelines/{id} Update pipeline
DELETE /api/pipelines/{id} Delete pipeline
GET /api/providers List service providers
POST /api/providers Create provider
GET /api/providers/{id} Get provider
PUT /api/providers/{id} Update provider
DELETE /api/providers/{id} Delete provider (fails if has models)
GET /api/models List models (optional ?api_kind=, ?capability=, ?provider_id=, ?search=)
GET /api/models/api-kinds List API kinds (chat-completions, embeddings, custom)
GET /api/models/capabilities List known capability tags
POST /api/models Create model (provider_id, name, api_kind, capabilities[], model_name)
GET /api/models/{id} Get model detail
PUT /api/models/{id} Update model
DELETE /api/models/{id} Delete model
POST /api/models/{id}/test Test model (proxies to provider; chat-completions with optional vision image)
GET /api/jobs List job runs (items, total, limit, offset; optional ?document_id=, ?knowledge_base_id=, ?connector_id=, ?status=, ?search=, ?limit=, ?offset=)
GET /api/jobs/{id} Get job run detail (includes events, and when the worker persisted output: worker_log merged command/stderr/stdout as plain text, worker_log_truncated, worker_log_char_limit; list endpoint omits log fields)
POST /api/jobs Queue processing run ({ document_id, pipeline_id?, force_reparse? }). force_reparse (default false): for pipeline document types, if storage already has {file_hash}/result.json from a prior successful parse, the worker reuses it and skips VLM (set true to always run the CLI). .xlsx: defers run_spreadsheet_preview (no pipeline_id; force_reparse ignored). .xmind: defers run_mindmap_preview (no pipeline_id; force_reparse ignored). Other extensions: requires channel or body pipeline_id; defers run_pipeline
POST /api/jobs/{id}/retry Retry a failed run; run_pipeline retries always set force_reparse so the VLM CLI runs again; run_kb_index re-queues the same knowledge_base_id; run_connector_sync re-queues the same connector_id (and start_date / end_date when present)
POST /api/jobs/{id}/mark-failed Mark a pending or running run failed when the worker stopped without finishing; reconciles linked document processing status from all runs for that document (does not downgrade completed when a later run succeeded)
DELETE /api/jobs/{id} Delete a run (not running)

Knowledge bases

Method Path Description
GET /api/knowledge-bases List knowledge bases
POST /api/knowledge-bases Create knowledge base
GET /api/knowledge-bases/{id} Get KB with stats
PUT /api/knowledge-bases/{id} Update KB (name, description, agent_url, chunk_config, embedding_model_id, faq_prompt, metadata_keys)
POST /api/knowledge-bases/{id}/index-job Queue run_kb_index worker job (openkms-cli kb-index); returns JobResponse; requires embedding_model_id on the KB
POST /api/knowledge-bases/{id}/wiki-spaces/{wiki_space_id}/index-job Queue run_kb_wiki_space_index for one linked wiki space (replaces that space’s wiki chunks; one page per chunk when ≤ 8000 chars); returns JobResponse
DELETE /api/knowledge-bases/{id} Delete KB (cascades documents, FAQs, chunks)
GET /api/knowledge-bases/{id}/documents Paginated list of documents in KB (?offset=, limit 1–200)
POST /api/knowledge-bases/{id}/documents Add document to KB
DELETE /api/knowledge-bases/{id}/documents/{doc_id} Remove document from KB
GET /api/knowledge-bases/{id}/wiki-spaces List wiki spaces linked to the KB (for RAG indexing)
POST /api/knowledge-bases/{id}/wiki-spaces Link a wiki space ({ "wiki_space_id": "…" }); pages are included on next kb-index
DELETE /api/knowledge-bases/{id}/wiki-spaces/{wiki_space_id} Unlink wiki space and delete KB chunks whose wiki_page_id belongs to that space
GET /api/knowledge-bases/{id}/wiki-pages-for-index Paginated wiki pages with body from linked spaces (?offset=, limit 1–500); used by kb-index
GET /api/knowledge-bases/{id}/faqs List FAQs (paginated; ?offset=, ?limit=, ?current_for_rag_only=)
POST /api/knowledge-bases/{id}/faqs Create FAQ
POST /api/knowledge-bases/{id}/faqs/polish Polish a draft FAQ answer with the KB judge model (preview only; body: question, answer)
PUT /api/knowledge-bases/{id}/faqs/{faq_id} Update FAQ
DELETE /api/knowledge-bases/{id}/faqs/{faq_id} Delete FAQ
POST /api/knowledge-bases/{id}/faqs/generate Generate FAQ preview from documents via LLM (no DB save)
POST /api/knowledge-bases/{id}/faqs/batch Save selected FAQ pairs to KB
GET /api/knowledge-bases/{id}/chunks List chunks (paginated; ?offset=, ?limit=, ?current_for_rag_only=)
GET /api/knowledge-bases/{id}/chunks/{chunk_id} Get one chunk (same shape as list rows; for deep links from Q&A / search)
PUT /api/knowledge-bases/{id}/chunks/{chunk_id} Update chunk (content, doc_metadata). If content changes, the stored embedding is cleared until the next indexing job refreshes vectors
DELETE /api/knowledge-bases/{id}/chunks Delete all chunks
POST /api/knowledge-bases/{id}/chunks/batch Bulk create chunks with embeddings (kb-index); each item sets exactly one of document_id or wiki_page_id
PUT /api/knowledge-bases/{id}/faqs/batch-embeddings Bulk update FAQ embeddings (kb-index pipeline)
POST /api/knowledge-bases/{id}/search Semantic / hybrid search over chunks (documents + linked wiki pages) and FAQs; chunk hits may include wiki_page_id, wiki_space_id. Response results[] items may include optional operator fields: chunk_index (ordinal within the source document or wiki page for chunk hits), retrieval_mode (dense, hybrid, bm25_only, dense_fallback), retrieval_debug (stage ranks/scores, e.g. dense_rank, dense_similarity, bm25_rank, bm25_score, rrf_score, rerank_score, pipeline_stages). force_dense in the body skips the hybrid QA-agent path when the KB has an agent URL
POST /api/knowledge-bases/{id}/ask Proxy to QA agent (JSON: question, conversation_history, optional session_id opaque string forwarded as Langfuse session id)
POST /api/knowledge-bases/{id}/ask/stream Same body; NDJSON stream then done with sources: hybrid hits unless Page Index document_section applies, or successful run_cypher_tool returned graph rows — then ontology rows (object type list + per-query Cypher summary). Optional error {detail, answer?}
GET /api/knowledge-bases/{id}/agent-conversations List the current user's persisted KB Q&A chats (agent_conversations with surface=knowledge_base and matching context.knowledge_base_id; KB visibility rules)
POST /api/knowledge-bases/{id}/agent-conversations Create chat (optional JSON { "title"?: string })
DELETE /api/knowledge-bases/{id}/agent-conversations/{conversation_id} Delete chat (cascades messages)
PATCH /api/knowledge-bases/{id}/agent-conversations/{conversation_id} Update chat title
GET /api/knowledge-bases/{id}/agent-conversations/{conversation_id}/messages Paginated messages: query limit (default 100, max 500), offset; JSON { items, total, limit, offset }. Requires knowledge_bases:read and KB visibility (same as other KB routes).
DELETE /api/knowledge-bases/{id}/agent-conversations/{conversation_id}/messages/from/{message_id} Delete this message and all later messages (used by "regenerate")
POST /api/knowledge-bases/{id}/agent-conversations/{conversation_id}/messages Send a user turn: JSON content, stream (boolean), optional session_id. stream: true → application/x-ndjson: user (persisted row), forwarded delta / tool_*, then done with answer, sources, user, message (persisted assistant row). If the upstream stream closes without a terminal done line but partial text or tool rows were received, the backend still persists a done-shaped reply with optional stream_ended_without_agent_done: true. Requires knowledge_bases:read. Persists tool transcripts (wiki_tool_traces_v1), interleaved stream parts (wiki_assistant_stream_parts_v1 on tool_calls for UI replay), and references (kb_qa_sources_v1)
GET /api/knowledge-bases/{id}/faq-assist-conversations Same as …/agent-conversations but threads use surface=kb_faq (FAQ-assist / exploratory notes against the same qa-agent)
POST /api/knowledge-bases/{id}/faq-assist-conversations Create FAQ-assist chat
DELETE /api/knowledge-bases/{id}/faq-assist-conversations/{conversation_id} Delete FAQ-assist chat
PATCH /api/knowledge-bases/{id}/faq-assist-conversations/{conversation_id} Update title
GET /api/knowledge-bases/{id}/faq-assist-conversations/{conversation_id}/messages Paginated messages (same as KB Q&A list)
DELETE /api/knowledge-bases/{id}/faq-assist-conversations/{conversation_id}/messages/from/{message_id} Truncate from message
POST /api/knowledge-bases/{id}/faq-assist-conversations/{conversation_id}/messages Same streaming contract as KB agent-conversations POST

Evaluation

Method Path Description
GET /api/evaluations List evaluations (optional ?knowledge_base_id=); filtered by read ACL
POST /api/evaluations Create evaluation (body: name, knowledge_base_id, optional wiki_space_id, optional description); bootstraps owner ACL
GET /api/evaluations/{id} Get evaluation (read ACL)
PUT /api/evaluations/{id} Update evaluation (write ACL)
DELETE /api/evaluations/{id} Delete evaluation (manage ACL)
GET /api/evaluations/{id}/items List evaluation items (offset, limit default 10 max 200; response { items, total })
POST /api/evaluations/{id}/items Add evaluation item (query, expected_answer, optional topic, optional sort_order)
POST /api/evaluations/{id}/items/import Import items from CSV (multipart file; columns: topic, query, answer or expected_answer)
PUT /api/evaluations/{id}/items/{item_id} Update evaluation item
DELETE /api/evaluations/{id}/items/{item_id} Delete evaluation item
POST /api/evaluations/{id}/run Run evaluation; body { evaluation_type?: "search_retrieval" \| "qa_answer" \| "wiki_content_coverage" }; wiki_content_coverage requires wiki_space_id on the evaluation; persists run + per-item results
GET /api/evaluations/{id}/runs List saved runs (offset, limit)
GET /api/evaluations/{id}/runs/{run_id} Full run with item results
DELETE /api/evaluations/{id}/runs/{run_id} Remove a saved run (cascades evaluation_run_items)
GET /api/evaluations/{id}/runs/compare Compare two runs (run_a, run_b query params)
GET /api/evaluations/{id}/agent-conversations List persisted assistant chats for this evaluation (surface=evaluation; same message/stream contract as KB Q&A; requires auth + evaluation scope; linked KB must have agent_url)
POST /api/evaluations/{id}/agent-conversations Create chat (optional { "title" })
DELETE /api/evaluations/{id}/agent-conversations/{conversation_id} Delete chat
PATCH /api/evaluations/{id}/agent-conversations/{conversation_id} Update title
GET /api/evaluations/{id}/agent-conversations/{conversation_id}/messages Paginated messages (limit default 100 max 500, offset) → { items, total, limit, offset }
DELETE /api/evaluations/{id}/agent-conversations/{conversation_id}/messages/from/{message_id} Truncate from message
POST /api/evaluations/{id}/agent-conversations/{conversation_id}/messages Same body/stream behavior as POST /api/knowledge-bases/.../agent-conversations/.../messages (proxies to that KB’s qa-agent)

Glossaries

Method Path Description
GET /api/glossaries List glossaries (read ACL filter)
POST /api/glossaries Create glossary; bootstraps owner ACL
GET /api/glossaries/{id} Get glossary with term count (read ACL)
PUT /api/glossaries/{id} Update glossary (write ACL)
DELETE /api/glossaries/{id} Delete glossary (manage ACL; cascades terms)
GET /api/glossaries/{id}/terms List terms (optional ?search=)
POST /api/glossaries/{id}/terms/suggest AI suggest translation, definition, synonyms (body: { primary_en?, primary_cn? }); uses default LLM
POST /api/glossaries/{id}/terms Create term
GET /api/glossaries/{id}/terms/{term_id} Get term
PUT /api/glossaries/{id}/terms/{term_id} Update term
DELETE /api/glossaries/{id}/terms/{term_id} Delete term
GET /api/glossaries/{id}/export Export terms as JSON
POST /api/glossaries/{id}/import Import terms (body: { terms, mode: "append" \| "replace" })
Method Path Description
GET /api/object-types List object types (authenticated, read ACL); ?count_from_neo4j=true for instance_count from Neo4j
POST /api/object-types Create object type (admin-only); bootstraps owner ACL. Accepts optional api_name (the frozen identifier code/Cypher use): generated from name when it has ASCII parts, required when it has none, 400 when malformed or taken. neo4j_label is set to it. key_property / display_property must be declared properties
GET /api/object-types/{id} Get object type; ?count_from_neo4j=true for instance_count from Neo4j
PUT /api/object-types/{id} Update object type (admin-only). Renaming name keeps api_name and neo4j_label — display only. api_name is accepted only while unset (a row predating the column); sending it for a row that has one is 400
DELETE /api/object-types/{id} Delete object type (admin-only)
GET /api/object-types/{id}/objects List object instances from the graph only (read ACL on parent type). ?search= and ?prop.<name>= run in Neo4j, so ?limit=/?offset= and total describe the filtered set. 409 when no Neo4j data source is configured; 503 when the graph is unreachable; indexed: false when the type has rows to index but the label is empty
POST /api/object-types/{id}/objects Create object instance into apply queue + same-request Neo4j sync when DS exists (admin-only + write ACL; no-dataset types only)
GET /api/object-types/{id}/objects/{obj_id} Get object instance from the graph (read ACL). 404 when the node is absent — including an object that exists only in the write queue (index the type first)
PUT /api/object-types/{id}/objects/{obj_id} Update object instance queue row + same-request Neo4j sync (admin-only + write ACL; no-dataset types only)
DELETE /api/object-types/{id}/objects/{obj_id} Delete queue row + same-request Neo4j DETACH DELETE (admin-only + write ACL; no-dataset types only)
POST /api/object-types/{id}/purge-instances Dangerous. Clear queue rows for one object type and DETACH DELETE its Neo4j label. Body: { confirm: "PURGE {ObjectTypeName}", neo4j_data_source_id? }.
POST /api/object-types/index-to-neo4j Index object types that have a linked dataset or stored instances to Neo4j as nodes (admin-only; body: neo4j_data_source_id). ?prune=true also deletes nodes whose source row is gone and reports nodes_pruned (declines, with prune_skipped, when the source yields no rows)
POST /api/object-types/{id}/index-to-neo4j Index one object type to Neo4j from its dataset, or drain the object_instances apply queue when there is no dataset (admin-only; body: neo4j_data_source_id; 400 if neither applies). Accepts ?prune=true
GET /api/link-types List link types (authenticated); ?count_from_neo4j=true for link_count from Neo4j
POST /api/link-types Create link type (admin-only). Same api_name rules as object types; neo4j_rel_type is set to it
GET /api/link-types/{id} Get link type; ?count_from_neo4j=true for link_count from Neo4j
PUT /api/link-types/{id} Update link type (admin-only). name is display only; api_name is claimable only while unset
DELETE /api/link-types/{id} Delete link type (admin-only)
GET /api/link-types/{id}/links List link instances from the graph only. ?limit=, ?offset=, optional ?source_object_id= / ?source_key_value= filter on the source endpoint's id (the id the object API shows). 409 without a Neo4j data source; 503 when the graph is unreachable; indexed: false when the junction/source-FK/saved-link rows exist but no edges do
POST /api/link-types/{id}/links Create link instance (admin-only; rejected when link type uses junction dataset). Endpoints are looked up in the graph by object id (409 when either is not indexed; either end may be dataset-backed). Writes link_instances and MERGEs the edge in the same request
DELETE /api/link-types/{id}/links/{link_id} Delete link instance (admin-only; rejected when link type uses junction dataset); deletes the edge too
POST /api/link-types/index-to-neo4j Index link types to Neo4j: M:M junction, M:1/1:M from source dataset when configured, else saved link instances (admin-only). ?prune=true also deletes edges whose endpoint pair the source no longer has
POST /api/link-types/{id}/index-to-neo4j Index one link type to Neo4j (same rules as bulk; 400 if nothing to index) (admin-only). Accepts ?prune=true
POST /api/ontology/explore Execute read-only Cypher query against Neo4j (body: { cypher }); used by Object Explorer
GET /api/ontology/groups/{id}/related Link types / Functions / Actions derived from the group's saved object types (ontology:read)
POST /api/ontology/action-types/{id}/execute Run Action: built-in object_create / object_modify / object_delete (no Function) or Function + audit + output.edits; response may include applied, and webhook when the Action registers one (best-effort, after commit)
PATCH /api/ontology/action-types/{id} Update Action; switching rule_type to built-in clears function_id when omitted. webhook_service_id + webhook_path + webhook_method (POST/PUT/PATCH/DELETE/GET) + webhook_body (envelope|object) register the destination (path must be inside the service's allowlist, {field} placeholders allowed; null detaches). Responses also carry function_external_services (services the bound Function calls) and last_webhook ({ok, status, error, at} — the newest delivery attempt)
POST /api/ontology/action-types Create Action (ontology:write); optional webhook_service_id / webhook_path
DELETE /api/ontology/action-types/{id} Delete Action (frees api_name; archive alone does not)
GET /api/ontology/external-services External HTTP services a Function may call (client.external("name")). Secrets are never returned — only has_secret (ontology:read)
POST /api/ontology/external-services Register one (ontology:write): {api_name, display_name, kind: http\|k8s_service, base_url? (http; **400** for a*.svc.cluster.localorigin — usek8s_service), cluster_id?/namespace?/service?/port?/base_path? (k8s_service), auth_kind: none\|bearer\|header\|hmac, auth_header_name?, secret?, allowed_paths[], timeout_seconds?}
PATCH /api/ontology/external-services/{id} Update; omit secret to keep it, send "" to clear
DELETE /api/ontology/external-services/{id} Remove
GET /api/ontology/external-services/k8s-clusters Clusters a k8s_service entry can point at (ontology:read)
POST /api/ontology/external-services/by-api-name/{api_name}/resolve Runtime-only: kind + transport (direct/backend), URL + decrypted secret for http, allowed paths (caller token required; recorded by the audit middleware)
POST /api/ontology/external-services/by-api-name/{api_name}/call Relays a k8s_service call through the cluster's kubeconfig: {method, path, query?, body?} → {status, body}. Allowlist re-checked server-side; credential applied here; 502 when the cluster call fails
GET /api/app-builder/apps List module apps (?status=); ontology:read
POST /api/app-builder/apps Create module app (bindings.k8s required; publishes immediately; needs console:kubernetes + ontology:write)
GET /api/app-builder/apps/{id} Published run document only (404 if draft)
GET/POST/PUT/PATCH/DELETE /api/app-builder/apps/{id}/proxy/{path} HTTP proxy to the bound Kubernetes Service (ontology:read, session cookie)
PATCH /api/app-builder/apps/{id} Update metadata / bindings.k8s
DELETE /api/app-builder/apps/{id} Delete app
POST /api/app-builder/apps/{id}/publish New bindings snapshot
POST /api/app-builder/apps/{id}/unpublish Clear published; status → draft
GET /api/app-builder/apps/{id}/versions List published versions
GET /api/data-sources List data sources (limit, offset; console:data_sources)
POST /api/data-sources Create data source (console:data_sources)
GET /api/data-sources/{id} Get data source (console:data_sources)
PUT /api/data-sources/{id} Update data source (console:data_sources)
DELETE /api/data-sources/{id} Delete data source (console:data_sources)
POST /api/data-sources/{id}/test Test connection (console:data_sources)
POST /api/data-sources/{id}/neo4j-delete-all Delete all nodes and relationships in Neo4j (console:data_sources, Neo4j only)

Kubernetes clusters (console:kubernetes)

Register existing clusters for later agent deploy tooling. Responses never include kubeconfig plaintext (kubeconfig_configured flag only). See Kubernetes clusters.

Method Path Description
GET /api/kubernetes-clusters List clusters (limit, offset; console:kubernetes)
POST /api/kubernetes-clusters Register cluster (body: name, kubeconfig, optional description, default_namespace, api_server, options)
GET /api/kubernetes-clusters/{id} Get one cluster (console:kubernetes)
PUT /api/kubernetes-clusters/{id} Update cluster; omit/"" kubeconfig to keep stored secret; optional api_server ("" = use kubeconfig URL)
DELETE /api/kubernetes-clusters/{id} Delete cluster registration
POST /api/kubernetes-clusters/{id}/test Test connection via Version API; updates last_tested_at / last_test_ok
GET /api/kubernetes-clusters/{id}/namespaces List namespaces (read-only)
GET /api/kubernetes-clusters/{id}/deployments List Deployments (?namespace=; defaults to cluster default_namespace)
GET /api/kubernetes-clusters/{id}/pods List Pods (?namespace=; defaults to cluster default_namespace)
GET /api/kubernetes-clusters/{id}/services List Services (?namespace=)
GET /api/kubernetes-clusters/{id}/pods/{pod} Pod detail (containers, image, status)
GET /api/kubernetes-clusters/{id}/pods/{pod}/logs Recent Pod logs (?namespace=, ?tail=, ?container=)
GET /api/kubernetes-clusters/{id}/services/{name} Service detail (ports, selector, labels)
GET /api/kubernetes-clusters/{id}/manifest Live resource YAML (?kind= Deployment|Service|Pod|ConfigMap|Secret, ?name=, ?namespace=); Secret values redacted
POST /api/kubernetes-clusters/{id}/apply Create or patch YAML (Deployment, Service, Pod, ConfigMap); body {yaml, namespace?}
POST /api/kubernetes-clusters/{id}/delete Delete one allowlisted object; body {kind, name, namespace?}
GET /api/kubernetes-clusters/{id}/secrets List Opaque Secrets (keys only; ?namespace=)
PUT /api/kubernetes-clusters/{id}/secrets/{name} Upsert Opaque Secret; body {set_values, remove_keys?} (empty value keeps existing key); project-managed Secrets → 403
DELETE /api/kubernetes-clusters/{id}/secrets/{name} Delete Opaque Secret (project-managed → 403)
GET /api/kubernetes-clusters/{id}/configmaps List ConfigMaps with data
PUT /api/kubernetes-clusters/{id}/configmaps/{name} Upsert ConfigMap
DELETE /api/kubernetes-clusters/{id}/configmaps/{name} Delete ConfigMap
GET /api/kubernetes-clusters/{id}/deployments/{name}/env Container env / envFrom
PUT /api/kubernetes-clusters/{id}/deployments/{name}/env Replace one container's env / envFrom; body {container, env, env_from}

Project deploy secrets (projects:read / projects:write)

Encrypted values; never returned. Write/sync/delete reject personal API key auth. Sync also requires console:kubernetes.

Method Path Description
GET /api/projects/{id}/deploy-secrets List (name, cluster, namespace, key_names, sync status)
POST /api/projects/{id}/deploy-secrets Create; body {name, cluster_id, namespace?, values}
PATCH /api/projects/{id}/deploy-secrets/{sid} Update; body {cluster_id?, namespace?, set_values?, remove_keys?}
DELETE /api/projects/{id}/deploy-secrets/{sid} Delete; optional ?delete_in_cluster=true
POST /api/projects/{id}/deploy-secrets/{sid}/sync Upsert labeled Opaque Secret on the cluster
POST /api/projects/{id}/kubernetes/dev-sync On-demand pack of project local_path into Deployment Pod container_path (needs projects:write + console:kubernetes); optional in-pod reload webhook

Connectors (connectors:read / connectors:write)

Auth: GET / HEAD on /api/connectors and /api/connectors/* require connectors:read or connectors:write. POST, PUT, and DELETE require connectors:write.

Method Path Description
GET /api/connectors/kinds List connector kinds (category: sync | search_tool), input fields, output slots (sync only; each slot may include dataset_schema, default_pg_schema, default_table_name), output_schema (search_tool), secret key names; optional ?category=
GET /api/connectors List connector instances; optional ?category=
POST /api/connectors/provision-dataset Create a PostgreSQL table matching a sync slot schema and register a Dataset row (body: kind, slot, data_source_id, optional schema_name, table_name, display_name); requires connectors:write and console:datasets
POST /api/connectors Create connector: sync kinds require outputs (slot → dataset id); bound datasets must match the slot dataset_schema when defined; search_tool kinds omit outputs
GET /api/connectors/{id} Get one connector (sync_schedule for sync kinds: enabled, cron, timezone from settings; next_run_at, last_run_at, last_status, last_job_id from central scheduled_triggers registry)
POST /api/connectors/{id}/sync Queue a sync connector job (run_connector_sync); returns { job_id } (202). Body optional: { start_date, end_date } (ISO dates, inclusive). Manual: pass both dates. No dates: each connector kind resolves its own default window (Tushare: incremental daily from last row through today). Tushare output slots (schema tushare): trade_calendar, stock_basic (one stock_basic call per sync), stock_trade_daily (daily OHLCV), daily_basic (valuation/liquidity metrics), stock_adj_daily (adj_factor), dividends (dividend by ex_date); respects settings.sync_api_min_interval_seconds and sync_trade_cal_min_interval_seconds (default 0.31 s ≈ 200 req/min); skips trade_cal when the calendar table already spans the window; on rate limits, short waits retry in-process and longer waits re-queue with scheduled_at
POST /api/connectors/{id}/search Run search_tool connector (body: { "query": "…", "params"?: { … } } one-shot input overrides for playground); returns normalized { query, search_intent?, results[], debug? } (debug: endpoint, request body, raw provider JSON)
POST /api/connectors/{id}/probe Tushare only: live daily (历史日线) probe from the detail Probe tab (body: { "api_name"?: "daily", "ts_code"?, "trade_date"?, "start_date"?, "end_date"?, "limit"?, "offset"? } — at least one of the date/code filters required); returns { api_name, params, row_count, truncated, rows[], debug? } (debug.endpoint: connector api_base_url, default https://api.tushare.pro); no dataset writes; 429 when rate-limited
PUT /api/connectors/{id} Update connector; replace inputs / outputs / settings when provided (output datasets validated against slot schema; sync settings.sync_schedule write-through to scheduled_triggers); merge secrets or send {} to clear secrets
DELETE /api/connectors/{id} Delete connector and its scheduled_triggers row
GET /api/schedules List central schedule registry (limit, offset; default 25, max 200); returns items, total, limit, offset. Permissions: connectors:read/write or projects:read/write; rows include connector_id, project_id, mode when applicable
PATCH /api/schedules/{id} Update schedule (enabled, cron, timezone; agent kinds also prompt); sync kinds mirror settings.sync_schedule on the connector (connectors:write or projects:write)
POST /api/schedules/{id}/run-now Queue the scheduled target now (run_connector_sync or run_scheduled_project_agent); returns { job_id } (202)
POST /internal-api/process-heartbeat Internal service client only; worker/scheduler liveness (role, instance_id); in-memory registry for Console health (no history table)
GET /api/datasets List datasets (data:read; filtered by dataset ACL) — optional data_source_id, domain, tag (JSONB containment), search (name / schema.table / description / tag), limit (≤500), offset; returns total for pagination
GET /api/datasets/facets The filter values the reader can see, with counts: {domains, tags, sources} — each [{value, count}], most-used first (data:read, ACL-filtered). Declared before /{dataset_id} so “facets” is not read as an id
GET /api/datasets/from-source/{id} List a PostgreSQL source's tables — names only, so it is a read (data:read + dataset ACL): a reader must be able to see what a source offers. It used to require data:write, which contradicted the pattern for GET /api/datasets/* and left readers blind. Registering one of them still needs data:write
POST /api/datasets Create dataset (data:write)
GET /api/datasets/{id} Get dataset (data:read; dataset ACL)
POST /api/datasets/{id}/aggregate Summarise a dataset table (data:read; dataset ACL) — body {group_by:["column" \| {column, granularity?}], aggregates:[{fn, column?, name?}], filters?, order_by?, limit?, offset?} → {sql, columns, rows, row_count, capped, duration_ms}. fn ∈ count (no column = COUNT(*)) / sum / avg / min / max; a granularity (day…year) is a query-time date_trunc and only valid on a temporal column; order_by.field is a group-by column or an aggregate name. Same operator vocabulary and statement timeout as the rows query; bad column/fn/grain answers 400 with the classified {code, message, kind} shape
POST /api/datasets/{id}/rows/query Browse a dataset's table (data:read; dataset ACL) — body {filters:[{field, op, value}], sort?, order?, limit?, offset?, after?} → {columns, rows, total, limit, offset, next_after, capped}. filters address the table's columns (AND-ed, the operator whitelist of semantic-models query; ≤ 10 conditions), total counts what they match, columns is always the table's so a filtered-out result still renders a header, and the read has a statement timeout. after requires sort (400 otherwise). A bad filter answers 400 with the classified {code, message, kind} shape (DATASET_ROWS_UNKNOWN_COLUMN / _BAD_OPERATOR / _INVALID_FILTER / _TOO_MANY_FILTERS, kind=request)
POST /api/datasets/{id}/rows/apply Apply batch row ops {ops:[{op,pk,values,expect}],dry_run} (data:edit; opt-in + PK + audit)
GET /api/datasets/{id}/metadata Column metadata (data:read; dataset ACL) — includes each column's maintained description
PUT /api/datasets/{id} Update dataset (partial — only the fields you send change) — catalog fields, primary_key_columns, unique_keys, ai_context, is_editable (data:write; write on dataset)
DELETE /api/datasets/{id} Delete dataset (data:write; manage on dataset)
GET /api/dataset-joins List relationships (dataset_id filter; data:read)
POST /api/dataset-joins Create a relationship {from_dataset_id, to_dataset_id, from_columns, to_columns, required} (validated; data:write)
GET /api/dataset-joins/{id} Get a relationship (data:read)
PUT /api/dataset-joins/{id} Update a relationship (data:write)
DELETE /api/dataset-joins/{id} Delete a relationship (data:write)
PUT /api/datasets/{id}/columns/{column} Set a physical column's description (data:write)
POST /api/dataset-ingests Start a file ingest (multipart file + data_source_id; data:write)
POST /api/dataset-ingests/{id}/preview Parse head + infer schema (data:write)
POST /api/dataset-ingests/{id}/load Create/replace the target table + register dataset (data:write)
GET /api/dataset-ingests/{id} Ingest status (data:write)
GET /api/datasets/{id}/profile Last computed profile (data:read)
POST /api/datasets/{id}/profile Recompute profile (data:write; write on dataset)
GET /api/datasets/{id}/schema-history Schema snapshots (data:read)
GET /api/console/audit-events The mutation log (console:permissions): newest first, filters actor_sub, resource_type, resource_id, method, path (substring), since, before (keyset cursor), limit. Every POST/PUT/PATCH/DELETE the app served — including 4xx and 5xx, with detail.error naming the exception when a handler crashed. Written by middleware, so no endpoint can forget it and a new feature needs no change; resource_id comes from the path, or from the id of the JSON response when the route created something; no request bodies are stored
GET /api/quality-issues The quality inbox (data:read, oldest-worst first): every enabled rule across datasets with the latest run that judged it — {dataset_id, dataset_name, name, rule_type, column_name, severity, status (passed\|failed\|error\|never), checked_count, failed_count, detail, error_message, last_run_at}, optional ?status=. Filtered by dataset ACL (an issue is a fact about its dataset), and a rule with no run findings never, which is not passed
GET /api/datasets/{id}/quality-rules List quality rules (data:read)
POST /api/datasets/{id}/quality-rules Create quality rule (data:write) — five declarative types, or sql with config.sql
POST /api/datasets/{id}/quality-rules/validate Dry run a sql rule (data:write): {sql, column_name?, expect?} → {ok, failed_count, value, expect, error} — value is the number the query returned, so the caller can say it in the rule's own direction (expect: zero = rows that break it; nonzero = coverage, where 0 fails). Same code path as running it, so a rule that validates cannot fail for a different reason later; a bad query is a 200 {ok: false} so the author sees the message
GET /api/datasets/{id}/quality-rules/{rule_id} One rule (data:read) — what the rule's own page addresses
PUT /api/datasets/{id}/quality-rules/{rule_id} Update quality rule (data:write)
DELETE /api/datasets/{id}/quality-rules/{rule_id} Delete quality rule (data:write)
POST /api/datasets/{id}/quality-rules/{rule_id}/run Run one rule (data:write) → the rule (with last_status); the numbers are in its history below
GET /api/datasets/{id}/quality-rules/{rule_id}/runs The rule's runs, newest first: {status, checked_count, failed_count, detail, error_message}. Keyset paging with ?limit= (the page size) and ?before=&before_id= — the created_at and id of the last row you have, because two runs can share an instant; one without the other is a 400
GET /api/datasets/{id}/quality-rules/{rule_id}/runs Rule run history (data:read)
GET /api/metrics List metrics (semantic_model_id / base_dataset_id filter; data:read)
POST /api/metrics Create a metric {name, semantic_model_id, base_dataset_id, type, expression:{dialects:[{dialect, expression}]}, datatype?, filters?, joins?, ai_context?} (data:write; base dataset must be in the model's scope)
GET /api/metrics/{metric_id} Get a metric (data:read)
PUT /api/metrics/{metric_id} Update a metric (data:write)
DELETE /api/metrics/{metric_id} Delete a metric (data:write)
GET /api/metrics/{metric_id}/compute Compute a metric — read-only, statement timeout + row cap; records a run (data:read)
GET /api/metrics/{metric_id}/runs Recent executions of a metric (data:read)
GET /api/semantic-models List semantic models (named scopes; data:read)
POST /api/semantic-models Create a semantic model {name, description?, dataset_ids, ai_context?} (data:write)
GET /api/semantic-models/{model_id} Get a semantic model (with its datasets; data:read)
PUT /api/semantic-models/{model_id} Update a semantic model (name / description / dataset_ids / ai_context) (data:write)
DELETE /api/semantic-models/{model_id} Delete a semantic model (cascades its fields/metrics) (data:write)
GET /api/semantic-models/{model_id}/fields List a model's fields (data:read)
POST /api/semantic-models/{model_id}/fields Create a field {dataset_id, name, column_name?, expression?, datatype?, description?, dimension?, ai_context?}; datatype = a DataType value, label, dimension = {is_time: boolean} only (Ossie) (data:write)
PUT /api/semantic-models/{model_id}/fields/{field_id} Update a field (data:write)
DELETE /api/semantic-models/{model_id}/fields/{field_id} Delete a field (data:write)
GET /api/semantic-models/{model_id}/export Export one model as a flat Ossie document (version/name/datasets/relationships/metrics + entity-level OPENKMS custom_extensions where needed; no ids) — ?format=json\|yaml (data:read)
POST /api/semantic-models/import Upsert a model (+ fields/metrics) from an Ossie document (data:write)
POST /api/semantic-models/{model_id}/query Answer one ad-hoc question (read-only, capped, audited): body {metric, group_by:[{field, granularity?}], filters:[{field, op, value}], order_by:[{field, direction}], limit} → {sql, columns, rows, row_count, capped, duration_ms}. Fields are addressed by id or name and may belong to the metric's base dataset or one it joins (the join is already in the SQL; a name that exists on both sides means the base's, and a name two joined datasets share is a 400 QUERY_AMBIGUOUS_FIELD — address those by id). Ops: = != > >= < <= like in between is_null is_not_null; order_by.field is value or a grouped field (data:read). Failures are classified — 400 with {code, message, kind, detail?}: kind is definition (the model author must fix it: QUERY_METRIC_NOT_AGGREGATE, QUERY_UNKNOWN_COLUMN/_TABLE/_FUNCTION, QUERY_TYPE_MISMATCH, QUERY_SYNTAX, QUERY_DEFINITION_INVALID), data (the source refused: QUERY_PERMISSION_DENIED, QUERY_TIMEOUT, QUERY_SOURCE_UNREACHABLE, QUERY_FAILED) or request (QUERY_UNKNOWN_METRIC/_FIELD/_DATASET/_AMBIGUOUS_FIELD, QUERY_ORDER_NOT_GROUPED, QUERY_INVALID_REQUEST); message is localized (app/i18n/catalog.py), detail holds the source's own text. Mapping is by SQLSTATE (app/services/data_layer/query_errors.py)
GET /api/stories List stories (insights:read) — optional search (name / question / narrative) and dataset_id (reverse lookup: stories with a finding on that dataset). A summary per story (finding_count + the sources it reads, named; no rows), because listing must not carry every finding's result. A story is listed while at least one of its findings (or none yet) reads a source the caller may read, and only the readable ones are counted and named
POST /api/stories Create story (insights:write) — body {name, question?, narrative?, findings?}; every finding in findings must name a source the caller may read
GET /api/stories/{id} Get story (insights:read) — findings are its tiles, each carrying its own source plus the last run's columns/rows/total/capped/duration_ms/sql/ran_at/error. A finding whose source the caller may not read answers with REPORT_SOURCE_FORBIDDEN instead of its rows
PUT /api/stories/{id} Update story (insights:write) — omit what you keep. A finding may keep a source the caller has since lost access to (reported per finding); a new binding must be readable
PUT /api/stories/{id} A finding is {title?, insight?, source_kind, dataset_id\|semantic_model_id, query, view?, …}. view is presentation only — {kind: "chart", type: "line"\|"bar", x, y, series?} | {kind: "metric", column, label?} | {kind: "bar_list", column, label_column?}, naming columns of that finding's result; absent = a table, and a view the result cannot satisfy falls back to one. A run merges into the finding ({**finding, **result}), so title/insight/query/view survive a re-run
POST /api/stories/{id}/run Run every finding against its own source and persist the results (insights:read — a refresh recomputes, it does not edit — plus each source's read ACL). A dataset finding goes to POST /api/datasets/{id}/rows/query (or /aggregate when its query carries aggregates), a model finding to POST /api/semantic-models/{id}/query. One finding failing — or being out of reach (REPORT_SOURCE_FORBIDDEN) — stores its classified {code, message, kind} and the rest still run (REPORT_SOURCE_MISSING / REPORT_METRIC_MISSING are request)
DELETE /api/stories/{id} Delete story (insights:write)

Feature toggles

Method Path Description
GET /api/feature-toggles Get feature toggle state (evaluations, connectors, agents, media, hasNeo4jDataSource; authenticated)
PUT /api/feature-toggles Update feature toggles (admin-only)

Console object storage

Method Path Description
GET /api/console/storage Bucket name and whether storage is configured (console:storage)
GET /api/console/storage/objects Paginated listing: prefix, continuation_token, max_keys (metadata only)
POST /api/console/storage/folders Create folder placeholder under parent_prefix + name
POST /api/console/storage/move Copy (and optionally delete) selected prefixes or objects to destination_prefix

Articles

Method Path Description
GET /api/article-channels List article channels (tree). Query: limit, offset (root pagination + subtrees; same shape as document channels)
GET /api/article-channels/{id} Get channel by ID
POST /api/article-channels Create channel
PUT /api/article-channels/{id} Update channel (name, description, parent_id, review_model_id, review_prompt, review_criteria)
POST /api/article-channels/{id}/reorder Move channel up or down among siblings (body: { direction })
POST /api/article-channels/merge Merge source channel into target (move articles, delete source; optional include_descendants)
DELETE /api/article-channels/{id} Delete channel (fails if has articles or sub-channels)
GET /api/articles?channel_id=&search=&offset=&limit= List articles; channel_id optional; search filters by name
GET /api/articles/stats Article counts for index page
GET /api/articles/{id} Get article with markdown
POST /api/articles Create article (channel_id, name, markdown, optional metadata, origin_article_id)
POST /api/articles/import Bulk import articles (multipart or JSON; preserves origin_article_id for provenance)
PATCH /api/articles/{id} Update article info (name, channel_id, metadata, origin_article_id)
PUT /api/articles/{id}/markdown Update article markdown (rewrites relative asset links to current bundle paths)
DELETE /api/articles/{id} Delete article and its MinIO bundle
PATCH /api/articles/{id}/lifecycle Update lifecycle fields (series_id, effective_from, effective_to, lifecycle_status)
GET /api/articles/{id}/relationships List outgoing and incoming article relationships
POST /api/articles/{id}/relationships Create outgoing edge (target_article_id, relation_type, optional note)
DELETE /api/articles/{id}/relationships/{relationship_id} Delete an outgoing relationship
POST /api/articles/{id}/review Run LLM rubric review (channel review_model_id / review_prompt / review_criteria; optional body model_id, prompt); persists to article_reviews
GET /api/articles/{id}/reviews List recent reviews (limit 1–50)
GET /api/articles/{id}/reviews/latest Latest review (result: overall_score, pass, summary, criteria[], suggestions[]); 404 if none
GET /api/articles/{id}/attachments List attachments for an article
POST /api/articles/{id}/attachments Upload attachment (multipart)
DELETE /api/articles/{id}/attachments/{attachment_id} Delete attachment from MinIO + DB
POST /api/articles/{id}/images Upload an image into the article bundle (used by paste / drag-and-drop in the editor)
GET /api/articles/{id}/files/{path} Stream an image or attachment from the article's MinIO bundle
GET /api/articles/{id}/versions List versions (metadata only)
POST /api/articles/{id}/versions Create explicit version (snapshot of current markdown + metadata)
GET /api/articles/{id}/versions/{version_id} Full version snapshot
POST /api/articles/{id}/versions/{version_id}/restore Restore working copy from version

Comments

Method Path Description
GET /api/comments List comments for a resource. Query: resource_type (article | document | knowledge_base | wiki_space | project), resource_id, limit, offset. Returns nested top-level items with replies[], plus total, avg_rank, rank_count
POST /api/comments Create top-level comment: resource_type, resource_id, body, rank (0–5, required)
POST /api/comments/{id}/replies Reply to a top-level comment: body only
PATCH /api/comments/{id} Author update: body; top-level may also update rank
DELETE /api/comments/{id} Author delete (cascades replies when top-level)

Wiki spaces

Method Path Description
GET /api/wiki-spaces List wiki spaces (limit, offset; ACL-filtered)
POST /api/wiki-spaces Create wiki space
GET /api/wiki-spaces/{id} Get wiki space
PATCH /api/wiki-spaces/{id} Update wiki space (name, description, optional semantic_similarity_threshold 0–1, semantic_match_top_k integer ≥ 1, semantic_embedding_model_id embedding ApiModel id or null to clear)
POST /api/wiki-spaces/{id}/semantic-index Offline: embed all pages using the space’s configured embedding model when set, otherwise the global default embedding ApiModel (wikis:write); sets last_semantic_index_at on success; returns { indexed, failed, embedding_model_id, embedding_model_label }; 503 if PostgreSQL cannot persist vector columns (pgvector not installed / $libdir/vector missing)
DELETE /api/wiki-spaces/{id} Delete wiki space (cascades pages, files, document links)
GET /api/wiki-spaces/{id}/graph Wiki page graph (nodes + edges) for the navigator
GET /api/wiki-spaces/{id}/pages List pages (paginated). Each item is metadata only (no body — avoids large payloads; use GET .../pages/{page_id} for markdown). Optional path_prefix, limit (1–500), offset (wikis:read)
GET /api/wiki-spaces/{id}/pages/semantic-matches Match for q with short-circuit: if string (title/path) matches are non-empty, only those are returned; else semantic_matched_pages when the space has indexed embeddings ({ page_id, similarity }, ordered by similarity). Uses the space’s semantic_similarity_threshold, semantic_match_top_k (optional query top_k overrides the cap), and the same embedding model family as indexing (WikiPage.embedding_model_id matches the resolved model). If no pages in the space have embeddings, only string matching applies (empty semantic list, semantic_skipped false). Query params: text_match_limit, optional top_k (wikis:read)
POST /api/wiki-spaces/{id}/pages Create page (path, title, optional body, metadata)
GET /api/wiki-spaces/{id}/pages/{page_id} Get page (with body and metadata)
PATCH /api/wiki-spaces/{id}/pages/{page_id} Update page (title, body, metadata)
DELETE /api/wiki-spaces/{id}/pages/{page_id} Delete page
GET /api/wiki-spaces/{id}/pages/by-path/{page_path:path} Resolve page by Obsidian-style path
PUT /api/wiki-spaces/{id}/pages/by-path/{page_path:path} Upsert page by path (create or update)
DELETE /api/wiki-spaces/{id}/pages/by-path/{page_path:path} Delete page by path
GET /api/wiki-spaces/{id}/pages/{page_id}/page-index PageIndex tree for in-page navigation
GET /api/wiki-spaces/{id}/files List stored files for the space (wiki_files): vault mirror paths (including .md), ad-hoc uploads, etc.
POST /api/wiki-spaces/{id}/files Upload file (multipart); vault-relative filename → mirror under wiki/{id}/vault/…, else wiki/{id}/files/{file_id}/…
GET /api/wiki-spaces/{id}/files/{file_id}/content Redirect to presigned object URL
DELETE /api/wiki-spaces/{id}/files/{file_id} Delete one stored file row (storage + DB); may be a vault .md/asset or an upload — same list as GET …/files
GET /api/wiki-spaces/{id}/documents List documents linked to this wiki space
POST /api/wiki-spaces/{id}/documents Link an existing document into this space
DELETE /api/wiki-spaces/{id}/documents/{document_id} Unlink a document (does not delete the document)
POST /api/wiki-spaces/{id}/import/vault Bulk import an Obsidian vault (zip / multi-file upload)
POST /api/wiki-spaces/{id}/import/vault/markdown-file Append a single markdown file from a vault upload

Projects (Agents workspace)

Deep Agents runtime in backend/app/services/deep_agents/. Disk root: OPENKMS_PROJECTS_ROOT/{project_id}/. Permissions: projects:read, projects:write. Feature toggle: agents.

Method Path Description
GET /api/projects List current user's projects (limit, offset)
POST /api/projects Create project (scaffolds AGENTS.md, .openkms/skills/; auto-installs skills where agent_skills.is_default=true). Optional body: git_url (HTTPS), git_branch, git_credential_id — clones into the workspace before scaffolding
GET /api/projects/{id} Get project
PATCH /api/projects/{id} Update name, description, slug, settings
DELETE /api/projects/{id} Delete project, on-disk folder, sessions (including session API keys and LangGraph checkpoints), schedules, ACL, and comments
GET /api/projects/{id}/files?path= List directory
GET /api/projects/{id}/files/content?path= Read file
PUT /api/projects/{id}/files/content Write file
POST /api/projects/{id}/files/upload Upload file (multipart; optional path query prefix; filename may include nested relative path for folder uploads)
DELETE /api/projects/{id}/files Delete file or directory
GET /api/projects/{id}/skills Installed skills metadata from projects.settings.installed_skills
POST /api/projects/{id}/skills/{skill_id}/install Install registry version into .openkms/skills/{skill_id}/ (body: optional version)
DELETE /api/projects/{id}/skills/{skill_id} Uninstall skill from project
GET/PATCH /api/projects/{id}/settings Project agent settings (web search, git identity, installed_skills)
GET/POST /api/projects/{id}/conversations List / create chat sessions (surface=project)
PATCH/DELETE /api/projects/{id}/conversations/{cid} Update title / delete session (409 while a non-stale last_turn.status=running; delete clears LangGraph checkpoint thread)
POST /api/projects/{id}/conversations/{cid}/suggest-title LLM title from message history
GET/POST /api/projects/{id}/conversations/{cid}/messages GET history: query limit, offset, optional tail=true (last limit messages; ignores offset) → { items, total, limit, offset }. POST body: content, optional stream, optional session_id (Langfuse), optional mode (plan | agent), optional attachments (list of { id, name, mime, size } staged image refs from the attachments route; text may be empty when images are present). stream: true → NDJSON (user, delta, tool_*, subagent_*, todo, interrupt, error, done). Interactive streams early-commit user + empty assistant (last_turn=running) and run the agent in a background task (client disconnect cancels NDJSON only). 409 if a non-stale turn is already running. On failure: persist assistant row + error { detail, message } (HTTP 200). Streamed assistant rows persist tool_calls (wiki_tool_traces_v1 plus wiki_assistant_stream_parts_v1 for interleaved text/tool UI replay) for UI replay; context.last_turn updated on the conversation
POST /api/projects/{id}/conversations/{cid}/attachments Upload one image (multipart file; PNG / JPEG / WebP / GIF, ≤ 8 MB) staged for the next turn → { id, name, mime, size, url }. Stored in object storage under agent-attachments/{cid}/, not the project workspace; url is a short-lived presigned link
DELETE /api/projects/{id}/conversations/{cid}/attachments/{attachment_id} Discard a staged image before sending (query mime); 409 once a message references it
DELETE /api/projects/{id}/conversations/{cid}/messages/from/{message_id} Delete this message and everything after it; clears LangGraph checkpoint thread (revert / regenerate). 409 while a non-stale turn is running
POST /api/projects/{id}/conversations/{cid}/messages/resume HITL resume (decision: approve/reject/edit/respond); same durable stream contract as POST messages; 409 if already running
GET/POST /api/projects/{id}/schedules List / create project agent cron schedules (projects:read / projects:write); kinds project_agent_stateless (new session each run) or project_agent_stateful (fixed conversation_id); body: display_name, mode, cron, timezone, prompt, enabled, optional on_run_completed (keep | delete, stateless only), optional conversation_id (stateful)
PATCH/DELETE /api/projects/{id}/schedules/{sid} Update prompt/cron/enabled or delete schedule
POST /api/projects/{id}/schedules/{sid}/run-now Queue run_scheduled_project_agent; returns { job_id } (202)
POST /api/projects/{id}/git/init Local git init
GET /api/projects/{id}/git/status Porcelain status (both columns, so staged vs unstaged is preserved), current branch, origin URL, ahead/behind vs upstream
GET /api/projects/{id}/git/log Recent commits (limit ≤ 50, default 30) with ref decorations
GET /api/projects/{id}/git/branches Local branches and the current one
GET /api/projects/{id}/git/diff Unified diff for path (query staged=true for the index); untracked files diff as a whole-file addition
POST /api/projects/{id}/git/commit Commit (message, optional paths to stage them first, or stage_all=true); with neither it commits what is already staged
POST /api/projects/{id}/git/stage Add paths to the index
POST /api/projects/{id}/git/unstage Remove paths from the index
POST /api/projects/{id}/git/discard Throw away working-tree changes for paths
POST /api/projects/{id}/git/clone Clone HTTPS URL into empty project (optional branch, optional credential_id for private repos)
POST /api/projects/{id}/git/remote Set origin (HTTPS only); initializes git if needed; stores settings.git.remote_url and optional credential_id
POST /api/projects/{id}/git/pull Pull (body: credential_id)
POST /api/projects/{id}/git/push Push (body: credential_id)
GET/POST/DELETE /api/user/git-credentials Manage encrypted HTTPS PATs (Profile UI)

Agent skills (global registry)

Disk root: OPENKMS_AGENT_SKILLS_ROOT/{skill_id}/{version}/. Permissions: same as projects (projects:read / projects:write). UI: Agents → Skills.

Method Path Description
GET /api/agent-skills List skills and versions
GET /api/agent-skills/{skill_id} Get one skill and its versions
POST /api/agent-skills Upload skill package (multipart: skill_id, version, optional display_name/notes, and either archive zip or files[] + relative_paths[])
PATCH /api/agent-skills/{skill_id} Update display_name, is_default, default_version
DELETE /api/agent-skills/{skill_id} Delete skill and all version files (409 if installed in a project)
DELETE /api/agent-skills/{skill_id}/versions/{version} Delete version (409 if default or installed in a project)

Each agent_skill_versions row includes content_hash, uploaded_by, uploaded_by_name.

Knowledge map

The bundled openkms-skill CLI exposes the same routes as the Console Knowledge Map: knowledge-map nodes … and knowledge-map resource-links … (see openkms-skill/references/REFERENCE.md).

Method Path Description
GET /api/knowledge-map/nodes/tree Full Knowledge Map tree for the navigator
POST /api/knowledge-map/nodes Create node (name, optional parent_id, description, sort_order)
PATCH /api/knowledge-map/nodes/{id} Update node (rename, move, reorder)
DELETE /api/knowledge-map/nodes/{id} Delete node and its subtree
GET /api/knowledge-map/resource-links List all resource–node mappings (filter client-side if needed)
PUT /api/knowledge-map/resource-links Replace the node mapping for a single resource (resource_type, resource_id, knowledge_map_node_id)
DELETE /api/knowledge-map/resource-links?resource_type=&resource_id= Unmap a resource from any node
GET /api/knowledge-map/overview Overview view: A2UI messages (published or synthesized) + tree + labeled resources + stale/unresolved (knowledge_map:read)
GET /api/knowledge-map/overview/status Stale vs live semantic content_hash; has_composition, published_at
POST /api/knowledge-map/overview/publish Body { a2ui_messages } — validate ids against live map and upsert singleton A2UI document (knowledge_map:write)
DELETE /api/knowledge-map/overview Remove published A2UI document (knowledge_map:write); Browse falls back to synthesized default
GET/POST /api/knowledge-map/overview/designer/conversations List / create Overview designer chats (surface=knowledge_map_overview)
GET /api/knowledge-map/overview/designer/session Messages for one chat (conversation_id optional)
DELETE /api/knowledge-map/overview/designer/conversations/{id} Delete designer chat
POST /api/knowledge-map/overview/designer/chat Body { messages, working_a2ui_messages?, stream, conversation_id? }; NDJSON stream (delta / tool_* / done / error), tool set_a2ui_messages (knowledge_map:write)
GET /api/knowledge-map/map-html/status Legacy HTML overview snapshot status
GET /api/knowledge-map/map-html Legacy cached HTML document
POST /api/knowledge-map/map-html/regenerate Legacy one-shot HTML rebuild
GET/POST/DELETE /api/knowledge-map/map-html/designer/* Legacy HTML designer chats / NDJSON chat
POST /api/knowledge-map/map-html/preview Legacy hydrate + sanitize draft HTML
POST /api/knowledge-map/map-html/publish Legacy save HTML snapshot
DELETE /api/knowledge-map/map-html Legacy remove HTML artifact