Skip to content

Latest commit

 

History

History
3477 lines (2565 loc) · 70.8 KB

File metadata and controls

3477 lines (2565 loc) · 70.8 KB

API Reference

Fortémi provides a RESTful API for AI-enhanced note management with semantic search capabilities.

Base URL: http://localhost:3000

OpenAPI Spec: openapi.yaml

Authentication

OAuth2 (Recommended)

The API supports full OAuth2 with Dynamic Client Registration (RFC 7591).

# 1. Discover endpoints
curl http://localhost:3000/.well-known/oauth-authorization-server

# 2. Register client
curl -X POST http://localhost:3000/oauth/register \
  -H "Content-Type: application/json" \
  -d '{"client_name": "My App", "grant_types": ["client_credentials"]}'

# 3. Get token
curl -X POST http://localhost:3000/oauth/token \
  -d "grant_type=client_credentials&client_id=xxx&client_secret=yyy"

OAuth2 Endpoints:

Endpoint Method Description
/.well-known/oauth-authorization-server GET OAuth2 discovery metadata
/.well-known/oauth-protected-resource GET Protected resource metadata
/oauth/authorize GET, POST Authorization endpoint
/oauth/register POST Dynamic client registration (RFC 7591)
/oauth/token POST Token endpoint
/oauth/introspect POST Token introspection (RFC 7662)
/oauth/revoke POST Token revocation (RFC 7009)

API Keys (Simple)

For trusted integrations, use API key authentication:

curl -H "Authorization: Bearer mm_key_xxx" \
  http://localhost:3000/api/v1/notes

API Key Management:

# List API keys
GET /api/v1/api-keys

# Create API key
POST /api/v1/api-keys
Content-Type: application/json

{
  "name": "My Integration Key",
  "expires_at": "2027-01-01T00:00:00Z"
}

# Revoke API key
DELETE /api/v1/api-keys/{id}

Notes

Create Note

POST /api/v1/notes
Content-Type: application/json
Authorization: Bearer <token>

{
  "content": "# My Note\n\nNote content in markdown...",
  "tags": ["project", "ideas"],
  "revision_mode": "full"
}

Parameters:

Field Type Required Description
content string Yes Markdown content
tags string[] No Tags to apply
revision_mode string No full (default), light, or none

Response (201 Created):

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "title": "AI-generated title",
  "content_original": "# My Note\n\n...",
  "content_revised": "# My Note\n\nEnhanced content...",
  "tags": ["project", "ideas"],
  "created_at_utc": "2026-01-24T12:00:00Z",
  "updated_at_utc": "2026-01-24T12:00:00Z"
}

Get Note

GET /api/v1/notes/{id}

Returns the full note with original and revised content, tags, and semantic links.

Update Note

PATCH /api/v1/notes/{id}
Content-Type: application/json

{
  "content": "Updated content...",
  "starred": true,
  "archived": false
}

Update Note Status

Quick endpoint for status-only updates:

PATCH /api/v1/notes/{id}/status
Content-Type: application/json

{
  "starred": true,
  "archived": false
}

Delete Note

DELETE /api/v1/notes/{id}

Soft-deletes the note. Can be restored later.

Restore Note

POST /api/v1/notes/{id}/restore

Restores a soft-deleted note.

Purge Note

POST /api/v1/notes/{id}/purge

Permanently deletes a note and all associated data.

Reprocess Note

POST /api/v1/notes/{id}/reprocess

Queues a note for AI reprocessing (re-embedding, re-revision).

List Notes

GET /api/v1/notes?limit=50&offset=0&filter=starred

Query Parameters:

Param Type Description
limit int Max results (default: 50)
offset int Pagination offset
filter string starred or archived
tags string Comma-separated tag filter
created_after ISO8601 Date filter
created_before ISO8601 Date filter

Bulk Create Notes

POST /api/v1/notes/bulk
Content-Type: application/json

{
  "notes": [
    {
      "content": "# Note 1",
      "tags": ["batch"]
    },
    {
      "content": "# Note 2",
      "tags": ["batch"]
    }
  ]
}

Bulk Reprocess Notes

POST /api/v1/notes/reprocess
Content-Type: application/json

{
  "limit": 500,
  "revision_mode": "light",
  "steps": ["embedding", "linking", "title"],
  "note_ids": ["550e8400-...", "660e8400-..."]
}

Queues NLP pipeline jobs for multiple notes at once. Useful after model changes or to backfill new features.

Parameters:

Field Type Required Description
limit int No Max notes to process (default: 500, max: 5000)
revision_mode string No full, light (default), or none
steps string[] No Steps to run: embedding, linking, title, concept_tagging, reference_extraction, metadata_extraction, document_type, revision, or all (default)
note_ids UUID[] No Specific note IDs to reprocess. If omitted, all active notes up to limit are processed.

Response:

{
  "queued": 42,
  "total": 42,
  "revision_mode": "light",
  "steps": ["embedding", "linking"]
}

Example:

# Reprocess all notes with embedding only
curl -X POST http://localhost:3000/api/v1/notes/reprocess \
  -H "Authorization: Bearer mm_key_xxx" \
  -H "Content-Type: application/json" \
  -d '{"steps": ["embedding"]}'

Note Versioning

Fortémi maintains dual-track versioning: original (user-written) and revised (AI-enhanced) histories.

List Note Versions

GET /api/v1/notes/{id}/versions

Returns all versions of a note with metadata.

Response:

{
  "versions": [
    {
      "version": 3,
      "created_at": "2026-01-24T15:30:00Z",
      "change_summary": "Updated section on authentication",
      "content_hash": "sha256:abc123..."
    },
    {
      "version": 2,
      "created_at": "2026-01-24T12:00:00Z",
      "change_summary": "Initial revision",
      "content_hash": "sha256:def456..."
    }
  ]
}

Get Specific Version

GET /api/v1/notes/{id}/versions/{version}

Returns the full content of a specific version.

Restore Version

POST /api/v1/notes/{id}/versions/{version}/restore

Restores a note to a previous version, creating a new version in the process.

Delete Version

DELETE /api/v1/notes/{id}/versions/{version}

Deletes a specific version (cannot delete current version).

Diff Versions

GET /api/v1/notes/{id}/versions/diff?from=2&to=3

Returns a unified diff between two versions.

Response:

{
  "from_version": 2,
  "to_version": 3,
  "diff": "--- Version 2\n+++ Version 3\n@@ -10,3 +10,4 @@\n-Old line\n+New line"
}

Note Provenance

Get Provenance Chain

GET /api/v1/notes/{id}/provenance

Returns the W3C PROV provenance chain showing the full AI processing history.

Response:

{
  "note_id": "550e8400-...",
  "provenance": [
    {
      "activity": "ai_revision",
      "agent": "ollama:llama3.2",
      "timestamp": "2026-01-24T12:00:00Z",
      "inputs": ["original_content"],
      "outputs": ["revised_content"],
      "parameters": {
        "model": "llama3.2",
        "temperature": 0.7
      }
    },
    {
      "activity": "embedding_generation",
      "agent": "ollama:mxbai-embed-large",
      "timestamp": "2026-01-24T12:01:00Z"
    }
  ]
}

File Attachments

Upload File Attachment

POST /api/v1/notes/{id}/attachments
Content-Type: multipart/form-data
Authorization: Bearer <token>

file=@photo.jpg

Upload a file attachment to a note. Supported file types include images (JPEG, PNG, GIF, WebP), documents (PDF, DOCX, TXT), and more.

Response (201 Created):

{
  "id": "660e8400-e29b-41d4-a716-446655440000",
  "note_id": "550e8400-e29b-41d4-a716-446655440000",
  "filename": "photo.jpg",
  "content_type": "image/jpeg",
  "size_bytes": 2457600,
  "created_at": "2026-01-24T12:00:00Z",
  "storage_path": "attachments/660e8400-e29b-41d4-a716-446655440000.jpg"
}

Example:

curl -X POST http://localhost:3000/api/v1/notes/550e8400-e29b-41d4-a716-446655440000/attachments \
  -H "Authorization: Bearer mm_key_xxx" \
  -F "file=@vacation-photo.jpg"

Upload Attachment (Multipart)

POST /api/v1/notes/{id}/attachments/upload
Content-Type: multipart/form-data
Authorization: Bearer <token>

file=@photo.jpg

Alternative multipart upload endpoint that supports larger files. Uses the same request format as the standard attachment upload but with a dedicated route.

Example:

curl -X POST http://localhost:3000/api/v1/notes/550e8400-e29b-41d4-a716-446655440000/attachments/upload \
  -H "Authorization: Bearer mm_key_xxx" \
  -F "file=@large-document.pdf"

List Note Attachments

GET /api/v1/notes/{id}/attachments

Returns all attachments for a specific note.

Response:

{
  "attachments": [
    {
      "id": "660e8400-...",
      "filename": "photo.jpg",
      "content_type": "image/jpeg",
      "size_bytes": 2457600,
      "created_at": "2026-01-24T12:00:00Z",
      "has_exif": true,
      "has_location": true
    },
    {
      "id": "770e8400-...",
      "filename": "document.pdf",
      "content_type": "application/pdf",
      "size_bytes": 524288,
      "created_at": "2026-01-24T13:00:00Z",
      "has_exif": false,
      "has_location": false
    }
  ]
}

Example:

curl http://localhost:3000/api/v1/notes/550e8400-e29b-41d4-a716-446655440000/attachments \
  -H "Authorization: Bearer mm_key_xxx"

Get Attachment

GET /api/v1/attachments/{id}

Returns the attachment record as JSON (metadata, not the binary file).

Example:

curl http://localhost:3000/api/v1/attachments/660e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer mm_key_xxx"

Download Attachment

GET /api/v1/attachments/{id}/download

Downloads the raw binary file content with appropriate Content-Type and Content-Disposition headers.

Response Headers:

  • Content-Type: Original file MIME type (e.g., image/jpeg)
  • Content-Disposition: attachment; filename="photo.jpg"
  • Content-Length: File size in bytes

Example:

curl -O http://localhost:3000/api/v1/attachments/660e8400-e29b-41d4-a716-446655440000/download \
  -H "Authorization: Bearer mm_key_xxx"

Get Attachment Metadata

GET /api/v1/attachments/{id}/metadata

Returns comprehensive metadata including EXIF data, location provenance, and processing status.

Response:

{
  "id": "660e8400-...",
  "filename": "photo.jpg",
  "content_type": "image/jpeg",
  "size_bytes": 2457600,
  "created_at": "2026-01-24T12:00:00Z",
  "exif": {
    "camera_make": "Apple",
    "camera_model": "iPhone 14 Pro",
    "capture_time": "2026-01-24T10:30:45Z",
    "gps_latitude": 37.7749,
    "gps_longitude": -122.4194,
    "gps_altitude": 15.5,
    "orientation": 1,
    "iso": 100,
    "focal_length": "6.86 mm",
    "exposure_time": "1/120",
    "f_number": 1.78
  },
  "provenance": {
    "device_id": "iPhone-12345",
    "device_name": "John's iPhone",
    "software": "iOS 17.2",
    "location": {
      "latitude": 37.7749,
      "longitude": -122.4194,
      "altitude": 15.5,
      "accuracy": 5.0
    }
  },
  "processing": {
    "ocr_completed": true,
    "thumbnail_generated": true,
    "embedding_generated": false
  }
}

Example:

curl http://localhost:3000/api/v1/attachments/660e8400-e29b-41d4-a716-446655440000/metadata \
  -H "Authorization: Bearer mm_key_xxx"

Delete Attachment

DELETE /api/v1/attachments/{id}

Permanently deletes an attachment and its associated file from storage.

Response (204 No Content)

Example:

curl -X DELETE http://localhost:3000/api/v1/attachments/660e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer mm_key_xxx"

Memory Search

Memory search enables temporal-spatial queries on file attachments based on when and where they were captured. Uses a single unified endpoint with parameter-based mode selection.

For comprehensive documentation, see Memory Search Guide.

Search Memories

GET /api/v1/memories/search

A single endpoint that switches between location, temporal, and combined modes based on which query parameters are provided.

Query Parameters:

Param Type Required Description
lat float Conditional Latitude in decimal degrees (-90 to 90). Required for location/combined mode.
lon float Conditional Longitude in decimal degrees (-180 to 180). Required for location/combined mode.
radius float No Search radius in meters (default: 1000)
start datetime Conditional Start of time range (ISO 8601 or flexible format). Required for time/combined mode.
end datetime Conditional End of time range (ISO 8601 or flexible format). Required for time/combined mode.

At least one search dimension is required: lat+lon for location, start+end for temporal, or all five for combined.

Mode Selection:

Parameters Provided Mode Description
lat + lon (+ optional radius) location Spatial search, nearest memories
start + end time Temporal search, memories in time range
All five combined Intersection of spatial + temporal
None 400 error At least one dimension required

Response:

{
  "mode": "location",
  "results": [
    {
      "provenance_id": "uuid",
      "attachment_id": "uuid",
      "note_id": "uuid",
      "filename": "IMG_1234.jpg",
      "content_type": "image/jpeg",
      "distance_m": 245.7,
      "capture_time_start": "2026-01-15T14:30:00Z",
      "capture_time_end": "2026-01-15T14:30:00Z",
      "location_name": "Eiffel Tower",
      "event_type": "photo"
    }
  ],
  "count": 1
}

Examples:

# Location search: memories within 1km of a point
curl "http://localhost:3000/api/v1/memories/search?lat=37.7749&lon=-122.4194&radius=1000" \
  -H "Authorization: Bearer mm_key_xxx"

# Temporal search: memories from January 2026
curl "http://localhost:3000/api/v1/memories/search?start=2026-01-01&end=2026-02-01" \
  -H "Authorization: Bearer mm_key_xxx"

# Combined search: near a location during a specific week
curl "http://localhost:3000/api/v1/memories/search?lat=37.7749&lon=-122.4194&radius=5000&start=2026-01-15&end=2026-01-20" \
  -H "Authorization: Bearer mm_key_xxx"

Get Memory Provenance

GET /api/v1/notes/{id}/memory-provenance

Returns the complete file provenance chain for a note's attachments, including location, device, and capture time information.

Response (when provenance exists):

{
  "note_id": "550e8400-...",
  "files": [
    {
      "attachment_id": "660e8400-...",
      "filename": "photo.jpg",
      "capture_time_start": "2026-01-24T10:30:45Z",
      "location": {
        "latitude": 37.7749,
        "longitude": -122.4194
      },
      "device_name": "iPhone 14 Pro",
      "event_type": "photo"
    }
  ]
}

Response (no provenance):

{
  "note_id": "550e8400-...",
  "files": []
}

Example:

curl http://localhost:3000/api/v1/notes/550e8400-e29b-41d4-a716-446655440000/memory-provenance \
  -H "Authorization: Bearer mm_key_xxx"

Create Provenance Location

POST /api/v1/provenance/locations
Content-Type: application/json

{
  "latitude": 48.8584,
  "longitude": 2.2945,
  "source": "gps_exif",
  "confidence": "high",
  "altitude_m": 35.0,
  "horizontal_accuracy_m": 10.0,
  "vertical_accuracy_m": 5.0,
  "heading_degrees": 180.0,
  "speed_mps": 0.0,
  "named_location_id": null
}

Response (201 Created):

{
  "id": "location-uuid"
}

Source values: gps_exif, device_api, user_manual, geocoded, ai_estimated, unknown Confidence values: high, medium, low, unknown

Create Named Location

POST /api/v1/provenance/named-locations
Content-Type: application/json

{
  "name": "Eiffel Tower",
  "location_type": "poi",
  "latitude": 48.8584,
  "longitude": 2.2945,
  "radius_m": 100.0,
  "address_line": "Champ de Mars, 5 Avenue Anatole France",
  "locality": "Paris",
  "country": "France",
  "country_code": "FR",
  "timezone": "Europe/Paris"
}

Response (201 Created):

{
  "id": "named-location-uuid",
  "slug": "eiffel-tower"
}

Location types: home, work, poi, city, region, country

Create Provenance Device

POST /api/v1/provenance/devices
Content-Type: application/json

{
  "device_make": "Apple",
  "device_model": "iPhone 15 Pro",
  "device_os": "iOS",
  "device_os_version": "17.2",
  "software": "Camera",
  "software_version": "17.2",
  "has_gps": true,
  "has_accelerometer": true,
  "device_name": "My iPhone"
}

Response (201 Created):

{
  "id": "device-uuid",
  "device_make": "Apple",
  "device_model": "iPhone 15 Pro"
}

Devices are deduplicated on (device_make, device_model). Registering the same make+model returns the existing device ID.

Create File Provenance

POST /api/v1/provenance/files
Content-Type: application/json

{
  "attachment_id": "attachment-uuid",
  "capture_time_start": "2026-01-15T14:30:00Z",
  "capture_time_end": "2026-01-15T14:30:00Z",
  "capture_timezone": "Europe/Paris",
  "time_source": "exif",
  "time_confidence": "high",
  "location_id": "location-uuid",
  "device_id": "device-uuid",
  "event_type": "photo",
  "event_title": "Sunset at Eiffel Tower",
  "event_description": "Sunset view from Trocadéro"
}

Response (201 Created):

{
  "id": "provenance-uuid"
}

Links an attachment to its spatial-temporal capture context. Use location and device IDs from the creation endpoints above.

Create Note Provenance

POST /api/v1/provenance/notes
Content-Type: application/json

{
  "note_id": "550e8400-...",
  "activity": "ai_revision",
  "agent": "ollama:llama3.2",
  "inputs": ["original_content"],
  "outputs": ["revised_content"],
  "parameters": {
    "model": "llama3.2",
    "temperature": 0.7
  }
}

Records a W3C PROV provenance entry for a note. Used to track AI processing, imports, and other activities that transform note content.

Response (201 Created):

{
  "id": "provenance-uuid"
}

Full Document Reconstruction

Get Full Document

GET /api/v1/notes/{id}/full

Reconstructs the full document from chunks, useful for notes split across multiple database records.

Response:

{
  "note_id": "550e8400-...",
  "full_content": "# Complete Document\n\n...",
  "chunk_count": 3,
  "total_length": 15234
}

Temporal Queries

Fortémi uses UUIDv7 for temporal ordering.

Timeline View

GET /api/v1/notes/timeline?limit=50&before=2026-01-24T12:00:00Z

Returns notes in temporal order based on UUIDv7 creation time.

Query Parameters:

Param Type Description
limit int Max results (default: 50)
before ISO8601 Notes created before this time
after ISO8601 Notes created after this time

Activity View

GET /api/v1/notes/activity?days=7

Returns note activity statistics over a time period.

Response:

{
  "period_days": 7,
  "notes_created": 42,
  "notes_updated": 18,
  "notes_deleted": 3,
  "daily_breakdown": [
    {
      "date": "2026-01-24",
      "created": 8,
      "updated": 4,
      "deleted": 1
    }
  ]
}

Knowledge Health Dashboard

Monitor the health and quality of your knowledge base.

Overall Knowledge Health

GET /api/v1/health/knowledge

Returns comprehensive knowledge base health metrics.

Response:

{
  "total_notes": 1523,
  "orphan_notes": 42,
  "stale_notes": 18,
  "unlinked_notes": 95,
  "avg_links_per_note": 3.2,
  "tag_coverage": 0.87,
  "last_activity": "2026-01-24T15:30:00Z"
}

Orphan Tags

GET /api/v1/health/orphan-tags

Lists tags that are defined but not used by any notes.

Stale Notes

GET /api/v1/health/stale-notes?days=180

Returns notes that haven't been updated in N days.

Unlinked Notes

GET /api/v1/health/unlinked-notes

Returns notes with no semantic links to other notes.

Tag Co-occurrence

GET /api/v1/health/tag-cooccurrence?min_count=5

Returns tag co-occurrence statistics for discovering tag relationships.

Response:

{
  "pairs": [
    {
      "tag_a": "machine-learning",
      "tag_b": "python",
      "count": 42,
      "correlation": 0.78
    }
  ]
}

Search

Hybrid Search

GET /api/v1/search?query=machine+learning&mode=hybrid&limit=20

Query Parameters:

Param Type Description
query string Search query (required)
mode string hybrid (default), fts, or semantic
limit int Max results (default: 20)
strict_filter object Strict tag filter (see below)

Response:

{
  "results": [
    {
      "note_id": "550e8400-...",
      "score": 0.85,
      "snippet": "...machine learning algorithms...",
      "title": "ML Research Notes",
      "tags": ["ml", "research"]
    }
  ],
  "total": 42
}

Search Modes:

  • hybrid: Combines FTS + semantic (best for most queries)
  • fts: Full-text search only (exact keyword matching)
  • semantic: Vector similarity only (conceptual matching)

Strict Tag Filtering

Apply guaranteed tag-based filtering before fuzzy search. Unlike query string filters, strict filters guarantee exact matches.

GET /api/v1/search?q=authentication&strict_filter=<json>

Pass the strict_filter parameter as a URL-encoded JSON string:

curl "http://localhost:3000/api/v1/search?q=authentication" \
  -H "Authorization: Bearer mm_key_xxx" \
  --data-urlencode "strict_filter={\"required_tags\":[\"project:matric\"],\"any_tags\":[\"priority:high\"],\"excluded_tags\":[\"status:archived\"]}"

The strict_filter JSON object supports:

{
  "required_tags": ["project:matric"],
  "any_tags": ["priority:high", "priority:critical"],
  "excluded_tags": ["status:archived"],
  "required_schemes": ["client-acme"]
}

Strict Filter Parameters:

Field Type Logic Description
required_tags string[] AND Notes MUST have ALL these tags
any_tags string[] OR Notes MUST have AT LEAST ONE of these
excluded_tags string[] NOT Notes MUST NOT have ANY of these
required_schemes string[] Isolation Notes ONLY from these vocabulary schemes
excluded_schemes string[] Exclusion Notes NOT from these schemes
min_tag_count int - Minimum number of tags required
include_untagged bool - Include notes with no tags (default: true)

Use Cases:

  • Client isolation: "required_schemes": ["client-acme"]
  • Project search: "required_tags": ["project:matric"]
  • Priority filter: "any_tags": ["priority:high", "priority:critical"]
  • Exclude drafts: "excluded_tags": ["draft", "wip", "internal"]

Advanced Filters (Query String)

GET /api/v1/search?query=api&tag:backend&created_after:2026-01-01

Filter syntax in query string (soft filtering, combined with fuzzy search):

  • tag:name - Filter by tag
  • collection:uuid - Filter by collection
  • created_after:ISO8601 - Date range
  • created_before:ISO8601 - Date range

Tags

List Tags

GET /api/v1/tags

Returns all tags with usage counts.

Get Note Tags

GET /api/v1/notes/{id}/tags

Returns all tags applied to a specific note.

Set Note Tags

PUT /api/v1/notes/{id}/tags
Content-Type: application/json

{
  "tags": ["updated", "tags"]
}

Replaces all tags for a note.

SKOS Concepts

Fortémi implements W3C SKOS (Simple Knowledge Organization System) for controlled vocabularies and semantic tagging.

Concept Schemes

Concept schemes are top-level vocabularies that organize related concepts.

List Concept Schemes

GET /api/v1/concepts/schemes

Create Concept Scheme

POST /api/v1/concepts/schemes
Content-Type: application/json

{
  "title": "Project Taxonomy",
  "description": "Controlled vocabulary for project classification",
  "namespace": "https://example.org/projects/"
}

Get Concept Scheme

GET /api/v1/concepts/schemes/{id}

Update Concept Scheme

PATCH /api/v1/concepts/schemes/{id}
Content-Type: application/json

{
  "title": "Updated Project Taxonomy",
  "description": "Updated description"
}

Get Top Concepts

GET /api/v1/concepts/schemes/{id}/top-concepts

Returns the top-level concepts in a scheme (concepts with no broader concepts).

Concepts

List/Search Concepts

GET /api/v1/concepts?scheme_id={scheme_id}&search=machine

Query Parameters:

Param Type Description
scheme_id UUID Filter by concept scheme
search string Search in labels and definitions
limit int Max results

Autocomplete Concepts

GET /api/v1/concepts/autocomplete?q=mach&scheme_id={scheme_id}

Fast autocomplete endpoint for UI type-ahead.

Create Concept

POST /api/v1/concepts
Content-Type: application/json

{
  "scheme_id": "550e8400-...",
  "pref_label": "Machine Learning",
  "alt_labels": ["ML", "Statistical Learning"],
  "definition": "A field of AI focused on learning from data",
  "notation": "ML-001"
}

Get Concept

GET /api/v1/concepts/{id}

Get Full Concept

GET /api/v1/concepts/{id}/full

Returns concept with all relationships (broader, narrower, related) and usage statistics.

Update Concept

PATCH /api/v1/concepts/{id}
Content-Type: application/json

{
  "pref_label": "Machine Learning (Updated)",
  "definition": "Updated definition"
}

Delete Concept

DELETE /api/v1/concepts/{id}

Deletes a concept. Fails if the concept is in use by notes.

Concept Relationships

Get Ancestors

GET /api/v1/concepts/{id}/ancestors

Returns all ancestor concepts in the hierarchy.

Get Descendants

GET /api/v1/concepts/{id}/descendants?depth=2

Returns all descendant concepts up to a specified depth.

Get Broader Concepts

GET /api/v1/concepts/{id}/broader

Returns immediate parent concepts.

Add Broader Concept

POST /api/v1/concepts/{id}/broader
Content-Type: application/json

{
  "broader_id": "550e8400-..."
}

Establishes a broader/narrower relationship.

Get Narrower Concepts

GET /api/v1/concepts/{id}/narrower

Returns immediate child concepts.

Add Narrower Concept

POST /api/v1/concepts/{id}/narrower
Content-Type: application/json

{
  "narrower_id": "550e8400-..."
}

Get Related Concepts

GET /api/v1/concepts/{id}/related

Returns associatively related concepts (not hierarchical).

Add Related Concept

POST /api/v1/concepts/{id}/related
Content-Type: application/json

{
  "related_id": "550e8400-..."
}

Note Tagging with Concepts

Get Note Concepts

GET /api/v1/notes/{id}/concepts

Returns all SKOS concepts applied to a note.

Tag Note with Concept

POST /api/v1/notes/{id}/concepts
Content-Type: application/json

{
  "concept_id": "550e8400-..."
}

Untag Note Concept

DELETE /api/v1/notes/{id}/concepts/{concept_id}

Governance

Get Governance Stats

GET /api/v1/concepts/governance

Returns governance and quality metrics for the concept system.

Response:

{
  "total_schemes": 5,
  "total_concepts": 342,
  "concepts_with_definitions": 298,
  "concepts_in_use": 215,
  "avg_hierarchy_depth": 3.2,
  "orphan_concepts": 12
}

Export

Export Scheme as Turtle

GET /api/v1/concepts/schemes/{id}/export/turtle

Exports a concept scheme in RDF Turtle format (W3C SKOS-compatible).

Export All Schemes as Turtle

GET /api/v1/concepts/schemes/export/turtle

Exports all concept schemes in a single RDF Turtle document.

Example:

curl http://localhost:3000/api/v1/concepts/schemes/export/turtle \
  -H "Authorization: Bearer mm_key_xxx" \
  -o all-schemes.ttl

SKOS Collections

SKOS Collections group related concepts for convenience (W3C SKOS Section 9).

List Collections

GET /api/v1/concepts/collections?scheme_id={scheme_id}

Create Collection

POST /api/v1/concepts/collections
Content-Type: application/json

{
  "scheme_id": "550e8400-...",
  "label": "Core ML Concepts",
  "description": "Essential machine learning concepts"
}

Get Collection

GET /api/v1/concepts/collections/{id}

Update Collection

PATCH /api/v1/concepts/collections/{id}
Content-Type: application/json

{
  "label": "Updated Collection Name"
}

Delete Collection

DELETE /api/v1/concepts/collections/{id}

Replace Collection Members

PUT /api/v1/concepts/collections/{id}/members
Content-Type: application/json

{
  "concept_ids": ["550e8400-...", "660e8400-..."]
}

Replaces all members of a collection.

Add Collection Member

POST /api/v1/concepts/collections/{id}/members/{concept_id}

Remove Collection Member

DELETE /api/v1/concepts/collections/{id}/members/{concept_id}

Document Types

List Document Types

GET /api/v1/document-types?category={category}

Returns all document types, optionally filtered by category.

Query Parameters:

Param Type Description
category string Filter by category (code, prose, config, markup, data, api-spec, iac, etc.)

Response:

{
  "document_types": [
    {
      "name": "rust",
      "display_name": "Rust",
      "category": "code",
      "file_extensions": [".rs"],
      "filename_patterns": ["Cargo.toml", "Cargo.lock"],
      "chunking_strategy": "syntactic",
      "is_system": true
    }
  ]
}

Get Document Type

GET /api/v1/document-types/:name

Returns details for a specific document type.

Response:

{
  "name": "rust",
  "display_name": "Rust",
  "category": "code",
  "description": "Rust programming language",
  "file_extensions": [".rs"],
  "filename_patterns": ["Cargo.toml", "Cargo.lock"],
  "content_magic": [],
  "chunking_strategy": "syntactic",
  "syntax_language": "rust",
  "embedding_model_hint": null,
  "is_system": true,
  "created_at": "2026-01-15T10:00:00Z"
}

Create Document Type

POST /api/v1/document-types
Content-Type: application/json

{
  "name": "my-custom-type",
  "display_name": "My Custom Type",
  "category": "custom",
  "description": "Custom document type for specialized content",
  "file_extensions": [".mytype"],
  "filename_patterns": ["*.mytype"],
  "content_magic": ["^MYTYPE:"],
  "chunking_strategy": "semantic",
  "syntax_language": null,
  "embedding_model_hint": null
}

Creates a custom document type.

Parameters:

Field Type Required Description
name string Yes Unique identifier (lowercase, hyphens)
display_name string Yes Human-readable name
category string Yes Category: code, prose, config, markup, data, api-spec, iac, database, shell, docs, package, observability, legal, communication, research, creative, media, personal, custom
description string No Description of the document type
file_extensions string[] No File extensions (e.g., [".rs", ".rust"])
filename_patterns string[] No Exact filename patterns (e.g., ["Cargo.toml"])
content_magic string[] No Regex patterns for content detection
chunking_strategy string Yes semantic, syntactic, fixed, per_section, whole
syntax_language string No Language for syntactic chunking
embedding_model_hint string No Recommended embedding model

Response (201 Created):

{
  "name": "my-custom-type",
  "display_name": "My Custom Type",
  "category": "custom",
  "is_system": false,
  ...
}

Update Document Type

PATCH /api/v1/document-types/:name
Content-Type: application/json

{
  "display_name": "Updated Display Name",
  "description": "Updated description",
  "file_extensions": [".mytype", ".mt"]
}

Updates a custom document type. System types cannot be updated.

Delete Document Type

DELETE /api/v1/document-types/:name

Deletes a custom document type. System types cannot be deleted.

Detect Document Type

POST /api/v1/document-types/detect
Content-Type: application/json

{
  "filename": "docker-compose.yml",
  "content": "version: '3.8'\nservices:"
}

Auto-detects document type from filename and/or content.

Parameters:

Field Type Required Description
filename string No Filename to analyze
content string No Content to analyze (first 1KB sufficient)

At least one of filename or content must be provided.

Response:

{
  "document_type": "docker-compose",
  "confidence": 0.9,
  "detection_method": "filename_pattern",
  "category": "iac",
  "chunking_strategy": "per_section",
  "alternatives": [
    {
      "document_type": "yaml",
      "confidence": 0.5,
      "detection_method": "extension"
    }
  ]
}

Detection Methods:

Method Confidence Description
filename_pattern 1.0 Exact pattern match (e.g., Dockerfile, docker-compose.yml)
extension 0.9 File extension match (e.g., .rs → rust)
content_magic 0.7 Content pattern recognition (e.g., openapi: → OpenAPI)
default 0.1 Fallback to generic type

Collections

Note collections organize notes into folders with hierarchy support.

List Collections

GET /api/v1/collections?parent_id=<uuid>

Create Collection

POST /api/v1/collections
Content-Type: application/json

{
  "name": "Work Projects",
  "description": "Work-related notes",
  "parent_id": null
}

Get Collection

GET /api/v1/collections/{id}

Update Collection

PATCH /api/v1/collections/{id}
Content-Type: application/json

{
  "name": "Updated Collection Name",
  "description": "Updated description"
}

Delete Collection

DELETE /api/v1/collections/{id}

Get Collection Notes

GET /api/v1/collections/{id}/notes

Returns all notes in a collection.

Export Collection as Markdown

GET /api/v1/collections/{id}/export?include_frontmatter=true&content=revised

Exports all notes in a collection as a single concatenated Markdown document with optional YAML frontmatter separators.

Query Parameters:

Param Type Description
include_frontmatter bool Include YAML frontmatter per note (default: true)
content string original or revised (default: revised)

Example:

curl "http://localhost:3000/api/v1/collections/550e8400-e29b-41d4-a716-446655440000/export" \
  -H "Authorization: Bearer mm_key_xxx" \
  -o collection-export.md

Move Note to Collection

POST /api/v1/notes/{note_id}/move
Content-Type: application/json

{
  "collection_id": "550e8400-..."
}

Links

Get Note Links

GET /api/v1/notes/{id}/links

Returns bidirectional semantic links:

{
  "outgoing": [
    {"to_note_id": "...", "score": 0.82, "kind": "semantic"}
  ],
  "incoming": [
    {"from_note_id": "...", "score": 0.78, "kind": "semantic"}
  ]
}

Get Note Backlinks

GET /api/v1/notes/{id}/backlinks

Returns only incoming links to a note.

Get Related Notes

GET /api/v1/notes/{id}/related?limit=10&min_score=0.3&context_summary=true

Discovers related notes by combining semantic similarity (vector search on embeddings) with direct graph links. Returns a unified, deduplicated list with an optional LLM-generated context summary explaining the thematic connection.

Query Parameters:

Param Type Description
limit int Maximum results (default: 10, max: 50)
min_score float Minimum similarity score (default: 0.3)
context_summary bool Include LLM context summary (default: false)

Response:

{
  "note_id": "...",
  "related": [
    {
      "note_id": "...",
      "score": 0.92,
      "snippet": "Related content...",
      "title": "Note Title",
      "tags": ["topic-a"],
      "source": "semantic"
    },
    {
      "note_id": "...",
      "score": 0.85,
      "snippet": "Linked note...",
      "title": null,
      "tags": [],
      "source": "link_outgoing"
    }
  ],
  "context_summary": "These notes are related because they discuss similar API concepts..."
}

The source field indicates how each related note was discovered: "semantic" (vector similarity), "link_outgoing" (direct outgoing graph link), or "link_incoming" (incoming graph link). When context_summary=true and an inference backend is available, the response includes an LLM-generated explanation of the thematic connection between the notes.

Graph Exploration

Explore Graph

GET /api/v1/graph/{id}?depth=2&max_nodes=50

Traverses semantic links to discover connected notes using recursive CTEs.

Query Parameters:

Param Type Description
depth int Maximum traversal depth (default: 2, max: 10)
max_nodes int Maximum nodes to return (default: 50, max: 1000)
min_score float Minimum link score threshold (default: 0.0)
max_edges_per_node int Maximum edges returned per node (optional)
edge_filter string Community filter: all (default), intra_community, inter_community
include_structural bool Include structural collection edges (default: true)

Response:

{
  "nodes": [
    {
      "id": "550e8400-...",
      "title": "Root Note",
      "depth": 0
    },
    {
      "id": "660e8400-...",
      "title": "Connected Note",
      "depth": 1
    }
  ],
  "edges": [
    {
      "from": "550e8400-...",
      "to": "660e8400-...",
      "score": 0.82
    }
  ]
}

Graph Topology Stats

GET /api/v1/graph/topology/stats

Returns graph topology statistics for the current memory archive.

Response:

{
  "total_notes": 1523,
  "total_links": 8712,
  "isolated_nodes": 42,
  "connected_components": 18,
  "avg_degree": 11.4,
  "max_degree": 87,
  "min_degree_linked": 1,
  "median_degree": 9.0,
  "linking_strategy": "snn_pfnet",
  "effective_k": 25
}

Example:

curl http://localhost:3000/api/v1/graph/topology/stats \
  -H "Authorization: Bearer mm_key_xxx"

Graph Diagnostics

GET /api/v1/graph/diagnostics?sample_size=1000

Returns graph quality diagnostics by sampling embedding pairs.

Query Parameters:

Param Type Description
sample_size int Number of random embedding pairs to sample (default: 1000, range: 10–10000)

Example:

curl "http://localhost:3000/api/v1/graph/diagnostics?sample_size=500" \
  -H "Authorization: Bearer mm_key_xxx"

Capture Diagnostics Snapshot

POST /api/v1/graph/diagnostics/snapshot
Content-Type: application/json

{
  "label": "pre-migration",
  "sample_size": 1000
}

Captures and stores a labeled diagnostics snapshot for later comparison.

Parameters:

Field Type Required Description
label string Yes Human-readable label for the snapshot
sample_size int No Embedding pairs to sample (default: 1000)

List Diagnostics Snapshots

GET /api/v1/graph/diagnostics/history?limit=20

Returns previously captured diagnostics snapshots.

Query Parameters:

Param Type Description
limit int Max snapshots to return (default: 20, max: 100)

Compare Diagnostics Snapshots

GET /api/v1/graph/diagnostics/compare?before={uuid}&after={uuid}

Compares two diagnostics snapshots and returns a diff.

Query Parameters:

Param Type Required Description
before UUID Yes Snapshot ID for the "before" state
after UUID Yes Snapshot ID for the "after" state

Recompute SNN Scores

POST /api/v1/graph/snn/recompute
Content-Type: application/json

{
  "k": 25,
  "threshold": 0.10,
  "dry_run": false
}

Recomputes Shared Nearest Neighbor (SNN) scores for all edges.

Parameters:

Field Type Required Description
k int No Number of nearest neighbors (default: adaptive from graph config)
threshold float No SNN threshold — edges below this are pruned (default: 0.10)
dry_run bool No Compute scores without updating/deleting anything (default: false)

PFNET Sparsify

POST /api/v1/graph/pfnet/sparsify
Content-Type: application/json

{
  "q": 2,
  "dry_run": false
}

Runs Pathfinder Network (PFNET) sparsification to remove redundant edges.

Parameters:

Field Type Required Description
q int No PFNET q parameter — higher values produce sparser graphs (default: 2, RNG-equivalent)
dry_run bool No Compute without deleting/updating edges (default: false)

Coarse Community Detection

POST /api/v1/graph/community/coarse
Content-Type: application/json

{
  "coarse_dim": 64,
  "similarity_threshold": 0.3,
  "resolution": 1.0
}

Runs Louvain community detection on a dimensionality-reduced (MRL) graph.

Parameters:

Field Type Required Description
coarse_dim int No MRL truncation dimension (default: 64, range: 2–768)
similarity_threshold float No Minimum cosine similarity for edge inclusion (default: 0.3)
resolution float No Louvain resolution parameter (default: from config)

Trigger Graph Maintenance

POST /api/v1/graph/maintenance
Content-Type: application/json

{
  "steps": ["normalize", "snn", "pfnet", "snapshot"]
}

Queues a graph maintenance job. Jobs are deduplicated — if one is already pending, returns 200 with "already_pending" status.

Parameters:

Field Type Required Description
steps string[] No Steps to run. Default: all steps. Valid values: normalize, snn, pfnet, snapshot

Response (201 Created — new job):

{
  "id": "job-uuid",
  "status": "queued",
  "steps": ["normalize", "snn", "pfnet", "snapshot"]
}

Response (200 OK — deduplicated):

{
  "id": null,
  "status": "already_pending"
}

Example:

# Run full maintenance pipeline
curl -X POST http://localhost:3000/api/v1/graph/maintenance \
  -H "Authorization: Bearer mm_key_xxx" \
  -H "Content-Type: application/json" \
  -d '{}'

# Run only SNN and PFNET steps
curl -X POST http://localhost:3000/api/v1/graph/maintenance \
  -H "Authorization: Bearer mm_key_xxx" \
  -H "Content-Type: application/json" \
  -d '{"steps": ["snn", "pfnet"]}'

Embedding Sets

Embedding sets allow creating isolated embedding spaces for multi-tenant or specialized use cases.

List Embedding Sets

GET /api/v1/embedding-sets

Create Embedding Set

POST /api/v1/embedding-sets
Content-Type: application/json

{
  "slug": "client-acme",
  "name": "ACME Corp Knowledge",
  "embedding_config_id": "550e8400-..."
}

Get Embedding Set

GET /api/v1/embedding-sets/{slug}

Update Embedding Set

PATCH /api/v1/embedding-sets/{slug}
Content-Type: application/json

{
  "name": "Updated Name"
}

Delete Embedding Set

DELETE /api/v1/embedding-sets/{slug}

List Embedding Set Members

GET /api/v1/embedding-sets/{slug}/members

Returns all notes in an embedding set.

Add Embedding Set Members

POST /api/v1/embedding-sets/{slug}/members
Content-Type: application/json

{
  "note_ids": ["550e8400-...", "660e8400-..."]
}

Remove Embedding Set Member

DELETE /api/v1/embedding-sets/{slug}/members/{note_id}

Refresh Embedding Set

POST /api/v1/embedding-sets/{slug}/refresh

Regenerates embeddings for all notes in the set.

List Embedding Configs

GET /api/v1/embedding-configs

Returns available embedding model configurations.

Get Default Embedding Config

GET /api/v1/embedding-configs/default

Get Embedding Config

GET /api/v1/embedding-configs/{id}

Returns details for a specific embedding configuration.

Create Embedding Config

POST /api/v1/embedding-configs
Content-Type: application/json

{
  "name": "Custom Config",
  "model": "mxbai-embed-large",
  "dimension": 1024,
  "provider": "ollama",
  "is_default": false
}

Update Embedding Config

PATCH /api/v1/embedding-configs/{id}
Content-Type: application/json

{
  "name": "Updated Config",
  "is_default": true
}

Delete Embedding Config

DELETE /api/v1/embedding-configs/{id}

Deletes a non-default embedding configuration.

Templates

List Templates

GET /api/v1/templates

Create Template

POST /api/v1/templates
Content-Type: application/json

{
  "name": "Meeting Notes",
  "content": "# Meeting: {{topic}}\n\nDate: {{date}}\n\n## Attendees\n{{attendees}}",
  "default_tags": ["meeting"]
}

Get Template

GET /api/v1/templates/{id}

Update Template

PATCH /api/v1/templates/{id}
Content-Type: application/json

{
  "name": "Updated Template Name",
  "content": "Updated template content"
}

Delete Template

DELETE /api/v1/templates/{id}

Instantiate Template

POST /api/v1/templates/{id}/instantiate
Content-Type: application/json

{
  "variables": {
    "topic": "Sprint Planning",
    "date": "2026-01-24",
    "attendees": "Alice, Bob"
  }
}

Creates a new note from the template with variables substituted.

Jobs

Background processing status for AI operations.

List Jobs

GET /api/v1/jobs?status=pending&job_type=ai_revision

Query Parameters:

Param Type Description
status string Filter by status: pending, processing, completed, failed
job_type string Filter by type: ai_revision, embedding, etc.
limit int Max results

Create Job

POST /api/v1/jobs
Content-Type: application/json

{
  "job_type": "ai_revision",
  "target_id": "550e8400-...",
  "parameters": {
    "mode": "full"
  }
}

Get Job

GET /api/v1/jobs/{id}

Pending Jobs Count

GET /api/v1/jobs/pending

Returns count of pending jobs.

Queue Stats

GET /api/v1/jobs/stats

Returns queue health metrics:

{
  "pending": 5,
  "processing": 2,
  "completed_last_hour": 150,
  "failed_last_hour": 0,
  "avg_processing_time_ms": 2341
}

Job Processing Control

Pause and resume job processing globally or per-archive.

Get Pause Status

GET /api/v1/jobs/status

Returns the current pause state and queue statistics:

{
  "global": "running",
  "archives": {
    "research": "paused"
  },
  "queue": {
    "pending": 42,
    "running": 3
  }
}

Pause All Processing

POST /api/v1/jobs/pause

Pauses job processing globally. Jobs already running will complete, but no new jobs will be picked up.

Resume All Processing

POST /api/v1/jobs/resume

Resumes globally paused job processing.

Pause Archive Processing

POST /api/v1/jobs/pause/{archive}

Pauses job processing for a specific memory archive. Jobs for other archives continue normally.

Resume Archive Processing

POST /api/v1/jobs/resume/{archive}

Resumes job processing for a specific memory archive.

Backup & Export

Fortémi provides multiple backup strategies for different use cases.

JSON Export/Import (Legacy)

Export Backup

GET /api/v1/backup/export

Exports all notes and metadata as JSON.

Download Backup

GET /api/v1/backup/download

Downloads the most recent export as a file.

Import Backup

POST /api/v1/backup/import
Content-Type: multipart/form-data

file=@backup.json

Imports notes from a JSON export.

Trigger Backup

POST /api/v1/backup/trigger

Manually triggers a backup job.

Backup Status

GET /api/v1/backup/status

Returns status of the most recent backup operation.

Knowledge Shards (Portable Exports)

Knowledge shards are application-level exports that include notes, concepts, and metadata but exclude embeddings.

Export Knowledge Shard

GET /api/v1/backup/knowledge-shard?format=json

Query Parameters:

Param Type Description
format string Export format: json or yaml
include_deleted bool Include soft-deleted notes

Import Knowledge Shard (Multipart Upload)

POST /api/v1/backup/knowledge-shard/upload?on_conflict=skip
Content-Type: multipart/form-data

file=@backup.shard

Query Parameters: on_conflict (skip/replace/merge), dry_run (bool), include (csv), skip_embedding_regen (bool)

Import Knowledge Shard (Legacy JSON)

POST /api/v1/backup/knowledge-shard/import
Content-Type: application/json

{"shard_base64": "...", "on_conflict": "skip"}

Database Backups (Full pg_dump)

Full PostgreSQL backups including embeddings and all data.

Download Database Backup

GET /api/v1/backup/database

Downloads a full pg_dump of the database.

Create Database Snapshot

POST /api/v1/backup/database/snapshot
Content-Type: application/json

{
  "label": "pre-migration-backup"
}

Creates a named database snapshot.

Upload Database Backup

POST /api/v1/backup/database/upload
Content-Type: multipart/form-data

file=@backup.sql

Uploads a database backup file for later restoration.

Restore Database Backup

POST /api/v1/backup/database/restore
Content-Type: application/json

{
  "filename": "backup_20260124_120000.sql"
}

Restores the database from a backup file. WARNING: This will overwrite all current data.

Memory-Scoped Backup

Download Memory Backup

GET /api/v1/backup/memory/{name}

Downloads a gzip-compressed pg_dump of a single memory archive schema. Unlike the full database backup, this exports only the specified memory's data.

Path Parameters:

Param Type Description
name string Memory archive name (e.g., work-notes)

Response Headers:

  • Content-Type: application/gzip
  • Content-Disposition: attachment; filename="memory_{name}_{timestamp}.sql.gz"

Example:

curl http://localhost:3000/api/v1/backup/memory/work-notes \
  -H "Authorization: Bearer mm_key_xxx" \
  -o work-notes-backup.sql.gz

Knowledge Archives

Knowledge archives bundle a knowledge shard with metadata in a single .archive file.

Download Knowledge Archive

GET /api/v1/backup/knowledge-archive/{filename}

Upload Knowledge Archive

POST /api/v1/backup/knowledge-archive
Content-Type: multipart/form-data

file=@knowledge-archive.archive

Backup Browser

List Backups

GET /api/v1/backup/list

Returns all available backup files.

Response:

{
  "backups": [
    {
      "filename": "backup_20260124_120000.sql",
      "size_bytes": 15234567,
      "created_at": "2026-01-24T12:00:00Z",
      "type": "database",
      "label": "pre-migration-backup"
    }
  ]
}

Get Backup Info

GET /api/v1/backup/list/{filename}

Returns detailed information about a specific backup file.

Swap Backup

POST /api/v1/backup/swap
Content-Type: application/json

{
  "backup_filename": "backup_20260124_120000.sql"
}

Swaps the current database with a backup (creates a backup of current state first).

Backup Metadata

Get Backup Metadata

GET /api/v1/backup/metadata/{filename}

Returns metadata for a backup file.

Update Backup Metadata

PUT /api/v1/backup/metadata/{filename}
Content-Type: application/json

{
  "label": "Updated label",
  "description": "Updated description",
  "tags": ["important", "pre-migration"]
}

Export

Export Note as Markdown

GET /api/v1/notes/{id}/export?content=revised&include_frontmatter=true

Returns markdown with YAML frontmatter suitable for Obsidian/Notion import.

Query Parameters:

Param Type Description
content string original or revised (default: revised)
include_frontmatter bool Include YAML frontmatter (default: true)

Memory Management

Fortemi supports parallel memory archives for organizing different knowledge domains with full schema-level isolation.

Request Header

All endpoints support memory routing via the X-Fortemi-Memory header:

Header Values Description
X-Fortemi-Memory Memory name Routes request to specified memory (default: "default")

List Memories

GET /api/v1/memories

Returns all memory archives with metadata (name, description, note count, size, schema version).

Create Memory

POST /api/v1/memories
Content-Type: application/json

{
  "name": "work-notes",
  "description": "Work-related documentation"
}

Response (201 Created):

{
  "id": "550e8400-...",
  "name": "work-notes",
  "schema_name": "archive_work_notes"
}

Returns HTTP 400 if MAX_MEMORIES limit is reached.

Get Memory

GET /api/v1/memories/:name

Update Memory

PATCH /api/v1/memories/:name
Content-Type: application/json

{
  "description": "Updated description"
}

Delete Memory

DELETE /api/v1/memories/:name

Permanently deletes the memory schema and all data. Cannot be undone.

Set Default Memory

POST /api/v1/archives/:name/default

Sets the specified memory as default. Only one memory can be default at a time.

Get Memory Statistics

GET /api/v1/archives/:name/stats

Returns note count and storage size for a specific memory.

Get Memories Overview

GET /api/v1/memories/overview

Returns aggregate statistics across all memories:

{
  "capacity": {
    "max_memories": 100,
    "current_count": 3,
    "available": 97
  },
  "usage": {
    "total_notes": 1768,
    "total_size_bytes": 60817408,
    "total_size_human": "58.02 MB"
  },
  "memories": [
    {
      "name": "default",
      "note_count": 1200,
      "size_bytes": 41943040,
      "is_default": true
    }
  ],
  "database": {
    "total_size_bytes": 209715200,
    "total_size_human": "200.00 MB"
  }
}

Clone Memory

POST /api/v1/archives/:name/clone
Content-Type: application/json

{
  "new_name": "work-notes-backup",
  "description": "Backup before migration"
}

Creates a deep copy of the memory including all notes, embeddings, tags, collections, and relationships.

Response (201 Created):

{
  "id": "660e8400-...",
  "name": "work-notes-backup",
  "schema_name": "archive_work_notes_backup",
  "cloned_from": "work-notes"
}

Federated Search

POST /api/v1/search/federated
Content-Type: application/json

{
  "query": "project documentation",
  "memories": ["default", "work-notes"]
}

Searches across multiple memories in parallel with unified result ranking.

Response:

{
  "results": [
    {
      "note_id": "...",
      "memory": "work-notes",
      "score": 0.92,
      "title": "Project Docs",
      "snippet": "...",
      "tags": ["project"]
    }
  ],
  "total": 1,
  "memories_searched": ["default", "work-notes"]
}

Use "memories": ["all"] to search every memory.

Vision

Ad-hoc image description using the configured vision LLM. Requires OLLAMA_VISION_MODEL to be set.

Describe Image

POST /api/v1/vision/describe
Content-Type: multipart/form-data
Authorization: Bearer <token>

file=@image.jpg

Analyzes an uploaded image and returns an AI-generated description. No attachment is stored — this is a stateless, ad-hoc operation.

Multipart Fields:

Field Type Required Description
file binary Yes Image file (JPEG, PNG, WebP, GIF, etc.)
prompt string No Custom description prompt
model string No Vision model override (e.g., llava:13b)

Response (200 OK):

{
  "description": "A sunset photograph showing orange and pink clouds over a city skyline...",
  "model": "qwen3.5:9b",
  "image_size": 2457600
}

Errors:

  • 400 Bad Request: Missing or empty file
  • 503 Service Unavailable: OLLAMA_VISION_MODEL not configured

Example:

curl -X POST http://localhost:3000/api/v1/vision/describe \
  -H "Authorization: Bearer mm_key_xxx" \
  -F "file=@sunset.jpg" \
  -F "prompt=Describe the colors and mood of this image"

Audio

Ad-hoc audio transcription using a Whisper-compatible backend. Requires WHISPER_BASE_URL to be set.

Transcribe Audio

POST /api/v1/audio/transcribe
Content-Type: multipart/form-data
Authorization: Bearer <token>

file=@recording.mp3

Transcribes an uploaded audio file and returns timestamped text segments. No attachment is stored — this is a stateless, ad-hoc operation.

Multipart Fields:

Field Type Required Description
file binary Yes Audio file (MP3, WAV, M4A, OGG, FLAC, etc.)
language string No ISO 639-1 language hint (e.g., en, es). Auto-detected if omitted.
model string No Whisper model override (e.g., Systran/faster-whisper-large-v3)

Response (200 OK):

{
  "text": "Hello, this is the full transcription of the audio file...",
  "segments": [
    {
      "start": 0.0,
      "end": 3.5,
      "text": "Hello, this is the full"
    },
    {
      "start": 3.5,
      "end": 7.2,
      "text": "transcription of the audio file..."
    }
  ],
  "language": "en",
  "duration_secs": 7.2,
  "model": "Systran/faster-distil-whisper-large-v3",
  "audio_size": 115200
}

Errors:

  • 400 Bad Request: Missing or empty file
  • 503 Service Unavailable: WHISPER_BASE_URL not configured

Example:

curl -X POST http://localhost:3000/api/v1/audio/transcribe \
  -H "Authorization: Bearer mm_key_xxx" \
  -F "file=@meeting-recording.mp3" \
  -F "language=en"

Chat

Synchronous LLM conversation endpoint. Bypasses the job queue and calls Ollama directly. GPU concurrency is gated by a semaphore (CHAT_MAX_CONCURRENT, default 1) — returns 503 when all inference threads are busy.

Send Chat Message

POST /api/v1/chat
Content-Type: application/json
Authorization: Bearer <token>

{
  "input": "What are the key themes across my recent notes?",
  "model": "qwen3.5:9b",
  "context": {
    "conversation_history": [
      {"role": "user", "content": "Tell me about my project notes"},
      {"role": "assistant", "content": "Based on your recent notes..."}
    ]
  }
}

Request Body:

Field Type Required Description
input string Yes The user's message
model string No Ollama model slug override (e.g., qwen3.5:9b). Uses server default if omitted.
context object No Optional context for the conversation
context.note_id string No Note ID for context (future RAG integration)
context.collection_id string No Collection ID for context (future RAG integration)
context.search_query string No Search query for context (future RAG integration)
context.conversation_history array No Previous messages for multi-turn conversation

Each message in conversation_history:

Field Type Required Description
role string Yes user or assistant
content string Yes Message content
timestamp string No ISO 8601 timestamp

Response (200 OK):

{
  "messages": [
    {
      "role": "assistant",
      "content": "Based on your recent notes, the key themes include..."
    }
  ],
  "actions": [],
  "model_info": {
    "model": "qwen3.5:9b",
    "context_window": 32768,
    "estimated_available_context": 32568,
    "max_output_tokens": 8192,
    "supports_thinking": true,
    "thinking_type": "explicit_tags",
    "speed_tok_s": 45.0,
    "parameter_size": "9B",
    "family": "qwen3.5"
  }
}

Model Info Fields:

Field Type Description
model string Model slug used for generation
context_window integer Total context window in tokens
estimated_available_context integer Context remaining after system prompt overhead
max_output_tokens integer Maximum output tokens per response
supports_thinking boolean Whether the model supports chain-of-thought reasoning
thinking_type string One of: explicit_tags, verbose_reasoning, pattern_based, none, not_tested
speed_tok_s float Estimated generation speed in tokens/second
parameter_size string Model parameter count (e.g., 8B, 70B)
family string Model family (e.g., qwen3, llama3)

Errors:

  • 400 Bad Request: Empty input
  • 503 Service Unavailable: Chat not configured (Ollama unreachable) or all GPU threads busy

When busy, the 503 response includes:

{
  "error": "Chat service is currently at capacity...",
  "retry_after": 5
}

Example:

# Simple chat
curl -X POST http://localhost:3000/api/v1/chat \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer mm_key_xxx" \
  -d '{"input": "Summarize my recent notes about Rust"}'

# With model selection and conversation history
curl -X POST http://localhost:3000/api/v1/chat \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer mm_key_xxx" \
  -d '{
    "input": "Tell me more about the async patterns",
    "model": "qwen3.5:9b",
    "context": {
      "conversation_history": [
        {"role": "user", "content": "What Rust topics have I been writing about?"},
        {"role": "assistant", "content": "Your recent notes cover async patterns, error handling, and trait design."}
      ]
    }
  }'

List Chat Models

GET /api/v1/chat/models
Authorization: Bearer <token>

Returns all installed Ollama models capable of chat (excludes embedding-only models), enriched with metadata from the model registry.

Response (200 OK):

{
  "models": [
    {
      "name": "qwen3.5:9b",
      "context_window": 32768,
      "max_output_tokens": 8192,
      "supports_thinking": true,
      "thinking_type": "explicit_tags",
      "speed_tok_s": 45.0,
      "parameter_size": "9B",
      "family": "qwen3.5",
      "size_bytes": 5400000000
    },
    {
      "name": "llama3.2:latest",
      "context_window": 131072,
      "max_output_tokens": 4096,
      "supports_thinking": false,
      "thinking_type": "none",
      "speed_tok_s": 0.0,
      "parameter_size": "",
      "family": "",
      "size_bytes": 2000000000
    }
  ],
  "default_model": "qwen3.5:9b"
}

Models without a registry profile return zeroed defaults for numeric fields and empty strings for text fields. The default_model field indicates which model the server uses when no model is specified in chat requests.

Errors:

  • 503 Service Unavailable: Chat not configured (Ollama unreachable)

Example:

curl http://localhost:3000/api/v1/chat/models \
  -H "Authorization: Bearer mm_key_xxx"

Inference

List Models

GET /api/v1/models

Returns all models available through the configured inference providers (Ollama, etc.).

Response:

{
  "models": [
    {
      "name": "llama3.2",
      "provider": "ollama",
      "capabilities": ["generation"],
      "size_bytes": 2000000000
    },
    {
      "name": "mxbai-embed-large",
      "provider": "ollama",
      "capabilities": ["embedding"],
      "size_bytes": 670000000
    }
  ]
}

Example:

curl http://localhost:3000/api/v1/models \
  -H "Authorization: Bearer mm_key_xxx"

Extraction Stats

GET /api/v1/extraction/stats

Returns analytics for extraction jobs including counts, durations, and breakdown by strategy.

Response:

{
  "total": 1523,
  "completed": 1488,
  "failed": 12,
  "pending": 23,
  "avg_duration_ms": 2341,
  "by_strategy": {
    "pdf": {"total": 412, "completed": 408, "failed": 4},
    "vision": {"total": 298, "completed": 295, "failed": 3},
    "text_native": {"total": 813, "completed": 785, "failed": 5}
  }
}

Example:

curl http://localhost:3000/api/v1/extraction/stats \
  -H "Authorization: Bearer mm_key_xxx"

PKE (Public Key Encryption)

Fortémi includes a Public Key Encryption system for secure note sharing. Keys use asymmetric cryptography so encrypted notes can be shared with specific recipients.

Generate Key Pair

POST /api/v1/pke/keygen

Generates a new PKE key pair. Returns the public key and a secure private key identifier.

Get Address

POST /api/v1/pke/address
Content-Type: application/json

{
  "public_key": "..."
}

Derives a shareable address from a public key.

Encrypt Note

POST /api/v1/pke/encrypt
Content-Type: application/json

{
  "note_id": "550e8400-...",
  "recipient_address": "addr_xxx"
}

Encrypts a note's content for a specific recipient address.

Decrypt Note

POST /api/v1/pke/decrypt
Content-Type: application/json

{
  "ciphertext": "...",
  "private_key_id": "key_xxx"
}

Decrypts ciphertext using the specified private key.

List Recipients

POST /api/v1/pke/recipients
Content-Type: application/json

{
  "note_id": "550e8400-..."
}

Returns the list of addresses that can decrypt a note.

Verify Address

GET /api/v1/pke/verify/{address}

Verifies that an address is valid and corresponds to a registered public key.

Keyset Management

PKE keysets bundle key pairs for management and export.

Endpoint Method Description
/api/v1/pke/keysets GET List all keysets
/api/v1/pke/keysets POST Create a new keyset
/api/v1/pke/keysets/active GET Get the currently active keyset
/api/v1/pke/keysets/import POST Import a keyset from JSON
/api/v1/pke/keysets/{name_or_id} DELETE Delete a keyset
/api/v1/pke/keysets/{name_or_id}/active PUT Set a keyset as active
/api/v1/pke/keysets/{name_or_id}/export GET Export a keyset as JSON

Real-Time Events

Fortémi provides real-time event streaming through three channels. For comprehensive documentation, see Real-Time Events.

SSE (Server-Sent Events)

GET /api/v1/events

Streams all server events as text/event-stream. Each event includes an event: type field and data: JSON payload. Keep-alive sent every 15 seconds.

WebSocket

GET /api/v1/ws

Full-duplex WebSocket connection receiving JSON-encoded events. Send "refresh" to trigger an immediate QueueStatus response.

Webhooks

Full CRUD for webhook subscriptions with event filtering and HMAC-SHA256 signing.

Endpoint Method Description
/api/v1/webhooks POST Create webhook subscription
/api/v1/webhooks GET List all webhooks
/api/v1/webhooks/{id} GET Get webhook details
/api/v1/webhooks/{id} PATCH Update webhook
/api/v1/webhooks/{id} DELETE Delete webhook
/api/v1/webhooks/{id}/deliveries GET List delivery logs
/api/v1/webhooks/{id}/test POST Send test delivery

Create Webhook:

POST /api/v1/webhooks
Content-Type: application/json

{
  "url": "https://example.com/webhook",
  "events": ["NoteUpdated", "JobCompleted", "JobFailed"],
  "secret": "optional-hmac-secret"
}

Event Types: 46 event types are supported, including NoteCreated, NoteUpdated, NoteDeleted, JobQueued, JobStarted, JobCompleted, JobFailed, and more. See Real-Time Events for the full list.

Webhook deliveries include X-Fortemi-Event header and optional X-Fortemi-Signature (HMAC-SHA256) when a secret is configured.

System

Memory Info

GET /api/v1/memory/info

Returns system memory usage information.

Response:

{
  "total_bytes": 16777216000,
  "used_bytes": 8388608000,
  "available_bytes": 8388608000,
  "percent_used": 50.0
}

Rate Limit Status

GET /api/v1/rate-limit/status

Returns current rate limit status for the authenticated client.

Response:

{
  "limit": 100,
  "remaining": 87,
  "reset_at": "2026-01-24T12:01:00Z",
  "retry_after_seconds": 45
}

Health

Health Check

GET /health

Returns 200 OK if the service is healthy. Includes version, database connectivity, and capability flags.

Response:

{
  "status": "ok",
  "version": "2026.1.0",
  "database": "connected",
  "ollama": "connected",
  "capabilities": {
    "extraction_strategies": ["pdf", "vision", "text_native", "audio", "code_ast"],
    "chat": {
      "available": true,
      "configured": true,
      "max_concurrent": 1
    }
  }
}

The chat capability reports:

  • configured: Whether an Ollama generation backend was reachable at startup
  • available: Whether at least one GPU semaphore permit is free (i.e., chat is not at capacity)
  • max_concurrent: The CHAT_MAX_CONCURRENT setting

Liveness Probe

GET /health/live

Minimal liveness probe for container orchestrators (Kubernetes, Docker Swarm). Returns 200 OK as long as the process is running, without checking downstream dependencies.

Error Responses

All errors follow a consistent format:

{
  "error": "not_found",
  "message": "Note not found",
  "details": {
    "note_id": "550e8400-..."
  }
}

Common Error Codes:

Status Error Description
400 bad_request Invalid request parameters
401 unauthorized Missing or invalid authentication
403 forbidden Insufficient permissions
404 not_found Resource not found
429 rate_limited Too many requests
500 internal_error Server error

Rate Limiting

  • Default: 100 requests/minute per API key
  • Search: 30 requests/minute
  • AI operations: 10 requests/minute

Rate limit headers:

  • X-RateLimit-Limit: Request limit
  • X-RateLimit-Remaining: Remaining requests
  • X-RateLimit-Reset: Reset timestamp

Versioning

The API is versioned via URL path (/api/v1/). Breaking changes will increment the version number.

See Also