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] } /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' } /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 } } 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 }