openapi: 3.0.3 info: title: Knowtation Hub API description: REST API for the Knowtation Hub (vault read/write, proposals, capture). Same contract as CLI/MCP where applicable. version: 1.0.0 links: - description: API contract (human-readable) url: ./HUB-API.md servers: - url: /api/v1 description: Relative to Hub base URL (e.g. https://hub.example.com) tags: - name: Health - name: Auth - name: Notes - name: Search - name: Proposals - name: Capture security: - BearerAuth: [] paths: /health: get: tags: [Health] summary: Health check security: [] responses: '200': description: Hub is up content: application/json: schema: { type: object, properties: { ok: { type: boolean } }, required: [ok] } /api/v1/auth/session: get: tags: [Auth] summary: C7 Session introspection — current identity and scopes description: | Returns the verified identity and derived API scopes for the caller. Accepts only a `Bearer` JWT in the `Authorization` header — no cookie required, making it safe to call cross-origin from Scooling or any other consumer. The response is derived entirely from the signed token — no database call is made. Scopes are role-derived today (C4 will replace this with per-user explicit grants without changing the response shape). security: - bearerAuth: [] responses: '200': description: Verified session content: application/json: schema: type: object required: [sub, provider, id, name, role, iat, exp, scopes] properties: sub: type: string description: Canonical user ID (`provider:id`) example: google:104164334692309763642 provider: type: string enum: [google, github] id: type: string description: Provider-specific user ID name: type: string description: Display name (empty for refresh-path tokens) role: type: string enum: [admin, member] iat: type: integer description: Token issued-at (Unix seconds) exp: type: integer description: Token expires-at (Unix seconds) scopes: type: array items: { type: string } description: Derived API scopes. `admin` role → `[vault:read, vault:write, admin]`; `member` → `[vault:read, vault:write]` example: [vault:read, vault:write] '401': description: Missing, expired, or tampered token content: application/json: schema: type: object properties: error: { type: string } code: { type: string, enum: [UNAUTHORIZED] } /auth/providers: get: tags: [Auth] summary: OAuth providers configured security: [] responses: '200': content: application/json: schema: type: object properties: google: { type: boolean } github: { type: boolean } /notes/facets: get: tags: [Notes] summary: Projects, tags, folders for filter dropdowns responses: '200': content: application/json: schema: type: object properties: projects: { type: array, items: { type: string } } tags: { type: array, items: { type: string } } folders: { type: array, items: { type: string } } /notes: get: tags: [Notes] summary: List notes parameters: - name: folder in: query schema: { type: string } - name: project in: query schema: { type: string } - name: tag in: query schema: { type: string } - name: since in: query schema: { type: string } - name: until in: query schema: { type: string } - name: limit in: query schema: { type: integer, minimum: 0, maximum: 100 } - name: offset in: query schema: { type: integer, minimum: 0 } - name: order in: query schema: { type: string, enum: [date, date-asc] } - name: fields in: query schema: { type: string, enum: [path, path+metadata, full] } - name: count_only in: query schema: { type: boolean } responses: '200': content: application/json: schema: oneOf: - type: object properties: notes: { type: array, items: { $ref: '#/components/schemas/NoteListItem' } } total: { type: integer } - type: object properties: total: { type: integer } post: tags: [Notes] summary: Write or update a note requestBody: required: true content: application/json: schema: type: object required: [path] properties: path: { type: string } body: { type: string } frontmatter: { type: object } append: { type: boolean } responses: '200': content: application/json: schema: { type: object, properties: { path: { type: string }, written: { type: boolean } } } '400': '500': content: application/json: schema: { $ref: '#/components/schemas/Error' } /notes/{path}: get: tags: [Notes] summary: Get one note by path parameters: - name: path in: path required: true schema: { type: string } responses: '200': content: application/json: schema: { $ref: '#/components/schemas/NoteFull' } '404': content: application/json: schema: { $ref: '#/components/schemas/Error' } /note-outline: get: tags: [Notes] summary: Body-free NoteOutline headings for one note description: > Returns knowtation.note_outline/v1 metadata for one authorized vault-relative note. The response excludes note body text, snippets, full frontmatter, absolute paths, provider payloads, MCP resource URIs, summaries, vectors, OCR, PageIndex output, persistence records, and write-back state. parameters: - name: path in: query required: true schema: { type: string } description: Vault-relative Markdown note path. responses: '200': content: application/json: schema: { $ref: '#/components/schemas/NoteOutline' } '400': content: application/json: schema: { $ref: '#/components/schemas/Error' } '401': content: application/json: schema: { $ref: '#/components/schemas/Error' } '403': content: application/json: schema: { $ref: '#/components/schemas/Error' } '404': content: application/json: schema: { $ref: '#/components/schemas/Error' } '502': content: application/json: schema: { $ref: '#/components/schemas/Error' } /document-tree: get: tags: [Notes] summary: Body-free DocumentTree heading hierarchy for one note description: > Returns knowtation.document_tree/v0 metadata for one authorized vault-relative note. The response excludes note body text, snippets, full frontmatter, absolute paths, provider payloads, MCP resource URIs, summaries, vectors, OCR, PageIndex output, persistence records, sidecars, LLM calls, and write-back state. parameters: - name: path in: query required: true schema: { type: string } description: Vault-relative Markdown note path. responses: '200': content: application/json: schema: { $ref: '#/components/schemas/DocumentTree' } '400': content: application/json: schema: { $ref: '#/components/schemas/Error' } '401': content: application/json: schema: { $ref: '#/components/schemas/Error' } '403': content: application/json: schema: { $ref: '#/components/schemas/Error' } '404': content: application/json: schema: { $ref: '#/components/schemas/Error' } '502': content: application/json: schema: { $ref: '#/components/schemas/Error' } /metadata-facets: get: tags: [Notes] summary: Body-free MetadataFacets hints for one note description: > Returns knowtation.metadata_facets/v0 metadata for one authorized vault-relative note. The response excludes note body text, snippets, full frontmatter, absolute paths, provider payloads, MCP resource URIs, summaries, labels, vectors, OCR, PageIndex output, media metadata, memory events, persistence records, sidecars, LLM calls, and write-back state. parameters: - name: path in: query required: true schema: { type: string } description: Vault-relative Markdown note path. responses: '200': content: application/json: schema: { $ref: '#/components/schemas/MetadataFacets' } '400': content: application/json: schema: { $ref: '#/components/schemas/Error' } '401': content: application/json: schema: { $ref: '#/components/schemas/Error' } '403': content: application/json: schema: { $ref: '#/components/schemas/Error' } '404': content: application/json: schema: { $ref: '#/components/schemas/Error' } '502': content: application/json: schema: { $ref: '#/components/schemas/Error' } /section-source: get: tags: [Notes] summary: Body-free SectionSource metadata for one note description: > Returns knowtation.section_source/v0 metadata for one authorized vault-relative note. The response excludes note body text, section body text, snippets, full frontmatter, line ranges, byte offsets, section body lengths, absolute paths, raw canister payloads, provider payloads, and MCP resource URIs. parameters: - name: path in: query required: true schema: { type: string } description: Vault-relative Markdown note path. responses: '200': content: application/json: schema: { $ref: '#/components/schemas/SectionSource' } '400': content: application/json: schema: { $ref: '#/components/schemas/Error' } '401': content: application/json: schema: { $ref: '#/components/schemas/Error' } '403': content: application/json: schema: { $ref: '#/components/schemas/Error' } '404': content: application/json: schema: { $ref: '#/components/schemas/Error' } '502': content: application/json: schema: { $ref: '#/components/schemas/Error' } /index: post: tags: [Notes] summary: Re-run indexer (vault to vector store) responses: '200': content: application/json: schema: type: object properties: ok: { type: boolean } notesProcessed: { type: integer } chunksIndexed: { type: integer } vectors_deleted: { type: integer, description: Rows removed for this vault before upsert (hosted sqlite-vec) } '500': content: application/json: schema: { $ref: '#/components/schemas/Error' } /export: post: tags: [Notes] summary: Export one note to content (returns body + filename for client download) requestBody: required: true content: application/json: schema: type: object required: [path] properties: path: { type: string } format: { type: string, enum: [md, html] } responses: '200': content: application/json: schema: type: object properties: content: { type: string } filename: { type: string } '400': content: application/json: schema: { $ref: '#/components/schemas/Error' } '404': content: application/json: schema: { $ref: '#/components/schemas/Error' } /import: post: tags: [Notes] summary: Import from uploaded file or ZIP (multipart: source_type; file except for google-sheets; optional project, tags, spreadsheet_id for google-sheets) requestBody: required: true content: multipart/form-data: schema: type: object required: [source_type] properties: source_type: type: string description: Importer id. For google-sheets, omit file and set spreadsheet_id; optional sheets_range (A1 notation). See lib/import-source-types.mjs. file: { type: string, format: binary, description: Required for all importers except google-sheets. } spreadsheet_id: type: string description: Required when source_type is google-sheets (id from the Google Sheets URL). sheets_range: type: string description: Optional for google-sheets; A1 range. Omit to read the first sheet from A1. project: { type: string } output_dir: { type: string } tags: { type: string } responses: '200': content: application/json: schema: type: object properties: imported: { type: array, items: { type: object } } count: { type: integer } '400': content: application/json: schema: { $ref: '#/components/schemas/Error' } '500': content: application/json: schema: { $ref: '#/components/schemas/Error' } /import-url: post: tags: [Notes] summary: Import from a public https URL (JSON body; editor/admin) requestBody: required: true content: application/json: schema: type: object required: [url] properties: url: { type: string, description: 'Full https URL' } mode: { type: string, enum: [auto, bookmark, extract], description: 'Capture mode (default auto)' } project: { type: string } output_dir: { type: string } tags: { oneOf: [{ type: string }, { type: array, items: { type: string } }] } responses: '200': content: application/json: schema: type: object properties: imported: { type: array, items: { type: object } } count: { type: integer } '400': content: application/json: schema: { $ref: '#/components/schemas/Error' } '500': content: application/json: schema: { $ref: '#/components/schemas/Error' } /settings: get: tags: [Notes] summary: Config status for Settings UI (no secrets) responses: '200': content: application/json: schema: type: object properties: vault_path_display: { type: string } vault_git: type: object properties: enabled: { type: boolean } has_remote: { type: boolean } auto_commit: { type: boolean } auto_push: { type: boolean } /vault/sync: post: tags: [Notes] summary: Manual vault backup (git add, commit, push) description: Self-hosted runs local git. Hosted (bridge) pushes notes as Markdown plus `.knowtation/backup/v1/snapshot.json` with full proposals. responses: '200': content: application/json: schema: type: object properties: ok: { type: boolean } message: { type: string } notesCount: { type: integer, description: Hosted bridge only } proposalsCount: { type: integer, description: Hosted bridge only } '400': content: application/json: schema: { $ref: '#/components/schemas/Error' } '500': content: application/json: schema: { $ref: '#/components/schemas/Error' } /search: post: tags: [Search] summary: Vault search (semantic or keyword) requestBody: required: true content: application/json: schema: type: object required: [query] properties: query: { type: string } mode: { type: string, enum: [semantic, keyword], description: Omitted or semantic = vector search; keyword = substring/token match on note text } match: { type: string, enum: [phrase, all_terms], description: Keyword only; phrase = full query substring; all_terms = every token must appear } folder: { type: string } project: { type: string } tag: { type: string } since: { type: string } until: { type: string } chain: { type: string } entity: { type: string } episode: { type: string } limit: { type: integer } order: { type: string } fields: { type: string } content_scope: { type: string, enum: [notes, approval_logs], description: Narrow to normal notes vs approvals/ logs } snippetChars: { type: integer } count_only: { type: boolean } countOnly: { type: boolean } responses: '200': content: application/json: schema: type: object properties: results: { type: array, items: { $ref: '#/components/schemas/SearchResult' } } query: { type: string } mode: { type: string, enum: [semantic, keyword] } count: { type: integer, description: Present when count_only keyword search } '400': content: application/json: schema: { $ref: '#/components/schemas/Error' } /proposals: get: tags: [Proposals] summary: List proposals parameters: - name: status in: query schema: { type: string } - name: limit in: query schema: { type: integer } - name: offset in: query schema: { type: integer } - name: label in: query description: Match if proposal labels include this string (case-insensitive) schema: { type: string } - name: source in: query schema: { type: string } - name: path_prefix in: query schema: { type: string } - name: evaluation_status in: query description: Filter by evaluation_status (none, pending, passed, failed, needs_changes) schema: { type: string } - name: review_queue in: query description: Exact match on proposal review_queue schema: { type: string } - name: review_severity in: query description: standard or elevated schema: { type: string } responses: '200': content: application/json: schema: type: object properties: proposals: { type: array, items: { $ref: '#/components/schemas/Proposal' } } total: { type: integer } post: tags: [Proposals] summary: Create proposal requestBody: content: application/json: schema: type: object properties: path: { type: string } body: { type: string } frontmatter: { type: object } intent: { type: string } base_state_id: { type: string } external_ref: { type: string } labels: { type: array, items: { type: string } } source: { type: string } responses: '201': content: application/json: schema: type: object properties: proposal_id: { type: string } path: { type: string } status: { type: string, enum: [proposed] } '400': /proposals/{id}: get: tags: [Proposals] summary: Get one proposal parameters: - name: id in: path required: true schema: { type: string } responses: '200': content: application/json: schema: { $ref: '#/components/schemas/ProposalDetail' } '404': /proposals/{id}/review-hints: post: tags: [Proposals] summary: Store async LLM review hints (canister; not a merge gate) parameters: - name: id in: path required: true schema: { type: string } requestBody: content: application/json: schema: type: object properties: review_hints: { type: string } review_hints_model: { type: string } responses: '200': content: application/json: schema: type: object properties: proposal_id: { type: string } ok: { type: boolean } /proposals/{id}/evaluation: post: tags: [Proposals] summary: Submit human evaluation (admin or evaluator) parameters: - name: id in: path required: true schema: { type: string } requestBody: content: application/json: schema: type: object required: [outcome] properties: outcome: type: string enum: [pass, fail, needs_changes] checklist: type: array items: type: object properties: id: { type: string } passed: { type: boolean } grade: { type: string } comment: { type: string } responses: '200': content: application/json: schema: { $ref: '#/components/schemas/ProposalDetail' } '400': '404': /proposals/{id}/approve: post: tags: [Proposals] summary: Apply proposal to vault parameters: - name: id in: path required: true schema: { type: string } requestBody: content: application/json: schema: type: object properties: base_state_id: { type: string } waiver_reason: type: string description: Admin override when evaluation is not passed (min length 3 after trim) external_ref: type: string description: Optional cross-system lineage id (e.g. Muse); server may resolve via MUSE_URL when omitted responses: '200': content: application/json: schema: type: object properties: proposal_id: { type: string } status: { type: string, enum: [approved] } external_ref: { type: string } '403': description: EVALUATION_REQUIRED — pass evaluation or provide waiver_reason '409': description: base_state_id mismatch (CONFLICT) /proposals/{id}/enrich: post: tags: [Proposals] summary: Optional LLM summary and suggested labels (KNOWTATION_HUB_PROPOSAL_ENRICH=1) parameters: - name: id in: path required: true schema: { type: string } responses: '200': content: application/json: schema: { $ref: '#/components/schemas/ProposalDetail' } '400': description: >- ICP canister — suggested_labels_json or assistant_suggested_frontmatter_json is valid JSON but exceeds max length (4000 / 14000 characters) after validation. '404': /proposals/{id}/discard: post: tags: [Proposals] summary: Discard proposal parameters: - name: id in: path required: true schema: { type: string } responses: '200': content: application/json: schema: type: object properties: proposal_id: { type: string } status: { type: string, enum: [discarded] } /capture: post: tags: [Capture] summary: Ingest message into vault inbox (webhook-style) description: Same contract as capture-webhook. If CAPTURE_WEBHOOK_SECRET is set, require X-Webhook-Secret header. security: [] requestBody: content: application/json: schema: type: object required: [body] properties: body: { type: string } source_id: { type: string } source: { type: string } project: { type: string } tags: { type: array, items: { type: string } } responses: '200': content: application/json: schema: { type: object, properties: { ok: { type: boolean }, path: { type: string } } } '400': components: securitySchemes: BearerAuth: type: http scheme: bearer bearerFormat: JWT schemas: Error: type: object properties: error: { type: string } code: { type: string } NoteListItem: type: object properties: path: { type: string } title: { type: string, nullable: true } project: { type: string, nullable: true } tags: { type: array, items: { type: string } } date: { type: string, nullable: true } NoteFull: type: object properties: path: { type: string } frontmatter: { type: object } body: { type: string } SearchResult: type: object properties: path: { type: string } snippet: { type: string } score: { type: number } project: { type: string } tags: { type: array, items: { type: string } } NoteOutline: type: object required: [schema, path, headings, truncated] properties: schema: type: string enum: [knowtation.note_outline/v1] path: { type: string } title: { type: string, nullable: true } headings: type: array maxItems: 500 items: { $ref: '#/components/schemas/NoteOutlineHeading' } truncated: { type: boolean } NoteOutlineHeading: type: object required: [level, text, id] properties: level: { type: integer, minimum: 1, maximum: 6 } text: { type: string } id: { type: string } DocumentTree: type: object required: [schema, path, root, truncated] properties: schema: type: string enum: [knowtation.document_tree/v0] path: { type: string } title: { type: string, nullable: true } root: type: object required: [children] properties: children: type: array maxItems: 500 items: { $ref: '#/components/schemas/DocumentTreeNode' } truncated: { type: boolean } DocumentTreeNode: type: object required: [id, level, text, children] properties: id: { type: string } level: { type: integer, minimum: 1, maximum: 6 } text: { type: string } children: type: array items: { $ref: '#/components/schemas/DocumentTreeNode' } MetadataFacets: type: object required: [schema, path, facets, inferred, truncated] properties: schema: type: string enum: [knowtation.metadata_facets/v0] path: { type: string } facets: type: object required: [project, tags, date, updated, causal_chain_id, entity, episode_id] properties: project: { type: string, nullable: true } tags: type: array maxItems: 100 items: { type: string } date: { type: string, nullable: true } updated: { type: string, nullable: true } causal_chain_id: { type: string, nullable: true } entity: type: array maxItems: 100 items: { type: string } episode_id: { type: string, nullable: true } inferred: type: object required: [folder, source_type] properties: folder: { type: string, nullable: true } source_type: { nullable: true, enum: [null] } truncated: { type: boolean } SectionSource: type: object required: [schema, path, sections, truncated] properties: schema: type: string enum: [knowtation.section_source/v0] path: { type: string } title: { type: string, nullable: true } sections: type: array items: { $ref: '#/components/schemas/SectionSourceSection' } truncated: { type: boolean } SectionSourceSection: type: object required: - section_id - heading_id - level - heading_path - heading_text - child_section_ids - body_available - body_returned - snippet_returned properties: section_id: { type: string } heading_id: { type: string } level: { type: integer, minimum: 1, maximum: 6 } heading_path: { type: array, items: { type: string } } heading_text: { type: string } child_section_ids: { type: array, items: { type: string } } body_available: { type: boolean } body_returned: { type: boolean, enum: [false] } snippet_returned: { type: boolean, enum: [false] } Proposal: type: object properties: proposal_id: { type: string } path: { type: string } status: { type: string } intent: { type: string } base_state_id: { type: string } external_ref: { type: string } vault_id: { type: string } proposed_by: { type: string } labels: { type: array, items: { type: string } } source: { type: string } suggested_labels: { type: array, items: { type: string } } assistant_notes: { type: string } assistant_model: { type: string } assistant_at: { type: string } created_at: { type: string } updated_at: { type: string } evaluation_status: type: string enum: [none, pending, passed, failed, needs_changes] evaluation_grade: { type: string } evaluation_comment: { type: string } evaluated_by: { type: string } evaluated_at: { type: string } evaluation_waiver: type: object nullable: true properties: by: { type: string } at: { type: string } reason: { type: string } review_queue: { type: string } review_severity: { type: string, enum: [standard, elevated] } auto_flag_reasons: type: array items: { type: string } auto_flag_reasons_json: { type: string, description: JSON array string on canister } review_hints: { type: string } review_hints_at: { type: string } review_hints_model: { type: string } assistant_suggested_frontmatter: type: object description: Normalized SPEC-aligned suggested note metadata from Enrich (object on GET); omitted or empty on older proposals additionalProperties: true ProposalDetail: allOf: - { $ref: '#/components/schemas/Proposal' } - type: object properties: body: { type: string } frontmatter: { type: object } evaluation_checklist: type: array items: type: object properties: id: { type: string } label: { type: string } passed: { type: boolean }