API Reference

REST API endpoints with request/response examples.

API Reference

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

Base URL: `http://localhost:3000`

This page is the curated consumer API reference published for hosted and self-hosted users. Fortemi does not publish an unauthenticated generated OpenAPI or AsyncAPI document.

Operators can fetch the full generated inventory with an admin-scoped bearer token:

curl -fsS \
  -H "Authorization: Bearer ${FORTEMI_ADMIN_TOKEN}" \
  http://localhost:3000/api/v1/operator/openapi.yaml

The equivalent AsyncAPI document is available at `/api/v1/operator/asyncapi.yaml`. An operator-only Swagger UI is mounted at `/api/v1/operator/docs`; it requires the same authorization and has `try_it_out` disabled. Direct browser access therefore requires an authenticated operator gateway that supplies the bearer credential.

The repository source artifact remains available for development review at openapi.yaml.

Error Contract: RFC 9457 Problem Details

Authentication

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:

EndpointMethodDescription
`/.well-known/oauth-authorization-server`GETOAuth2 discovery metadata
`/.well-known/oauth-protected-resource`GETProtected resource metadata
`/oauth/authorize`GET, POSTAuthorization endpoint
`/oauth/register`POSTDynamic client registration (RFC 7591)
`/oauth/token`POSTToken endpoint
`/oauth/introspect`POSTToken introspection (RFC 7662)
`/oauth/revoke`POSTToken revocation (RFC 7009)

API Keys (Simple)

For trusted integrations, use API key authentication:

curl -H "Authorization: Bearer <API_KEY>" \
  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 <ACCESS_TOKEN>

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

Parameters:

FieldTypeRequiredDescription
contentstringYesMarkdown content
tagsstring[]NoTags to apply
revision_modestringNo`full` (default), `light`, or `none`

Response (201 Created):

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "title": "AI-generated title",
  "content_original": "# My Note\
\
...",
  "content_revised": "# My Note\
\
Enhanced 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:

ParamTypeDescription
limitintMax results (default: 50)
offsetintPagination offset
filterstring`starred` or `archived`
tagsstringComma-separated tag filter
created_afterISO8601Date filter
created_beforeISO8601Date 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_generation"],
  "note_ids": ["550e8400-...", "660e8400-..."]
}

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

Parameters:

FieldTypeRequiredDescription
limitintNoInput note limit (default: 500, capped at 5000; zero or negative queues no work)
revision_modestringNo`full`, `light` (default), or `none`
stepsstring[]NoSteps to run: `embedding`, `linking`, `title_generation`, `concept_tagging`, `reference_extraction`, `metadata_extraction`, `document_type_inference`, `ai_revision`, `related_concept_inference`, `extraction`, or `all` (default)
note_idsUUID[]NoSpecific IDs; the first `limit` entries are checked for live notes in the authorized tenant and selected memory. If omitted, lists non-deleted notes up to `limit`, including archived notes.

Response:

{
  "message": "Bulk reprocessing queued",
  "notes_count": 42,
  "jobs_queued": 84,
  "revision_mode": "light"
}

Missing, soft-deleted, and out-of-scope IDs are silently skipped with the same behavior; the response exposes no per-ID reason or existence information. `notes_count` counts eligible input entries after filtering. Duplicate eligible IDs retain their input multiplicity; job deduplication can reduce `jobs_queued`. `jobs_queued` counts newly queued jobs, so existing pending/running jobs do not increase it. There is no skipped-count field. IDs beyond `limit` are not checked, and skipped entries inside the limit are not replaced by later IDs. An empty eligible set queues no jobs, including graph maintenance.

Eligibility is checked before any pipeline step is queued, using one bounded query for explicit IDs. This is not a deletion lock: a note can be deleted after selection or while queued. Workers retain their normal missing/deleted-note failure handling; already queued jobs are not canceled by this preflight.

Example:

# Reprocess all notes with embedding only
curl -X POST http://localhost:3000/api/v1/notes/reprocess \
  -H "Authorization: Bearer <API_KEY>" \
  -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\
+++ Version 3\
@@ -10,3 +10,4 @@\
-Old line\
+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 <ACCESS_TOKEN>

[email protected]

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 <API_KEY>" \
  -F "[email protected]"

Upload Attachment (Multipart)

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

[email protected]

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 <API_KEY>" \
  -F "[email protected]"

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 <API_KEY>"

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 <API_KEY>"

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`: metadata-only attachment filename, for example `attachment; filename="attachment_filename_len_9_660e8400-e29b-41d4-a716-446655440000"`
  • `Content-Length`: File size in bytes

Example:

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

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 <API_KEY>"

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 <API_KEY>"

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:

ParamTypeRequiredDescription
`lat`floatConditionalLatitude in decimal degrees (-90 to 90). Required for location/combined mode.
`lon`floatConditionalLongitude in decimal degrees (-180 to 180). Required for location/combined mode.
`radius`floatNoSearch radius in meters (default: 1000)
`start`datetimeConditionalStart of time range (ISO 8601 or flexible format). Required for time/combined mode.
`end`datetimeConditionalEnd 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 ProvidedModeDescription
`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
None400 errorAt 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 <API_KEY>"

# 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 <API_KEY>"

# 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 <API_KEY>"

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 <API_KEY>"

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\
\
...",
  "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:

ParamTypeDescription
limitintMax results (default: 50)
beforeISO8601Notes created before this time
afterISO8601Notes 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
    }
  ]
}
GET /api/v1/search?query=machine+learning&mode=hybrid&limit=20

Query Parameters:

ParamTypeDescription
querystringSearch query (required)
modestring`hybrid` (default), `fts`, or `semantic`
limitintMax results (default: 20)
strict_filterobjectStrict 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 <API_KEY>" \
  --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:

FieldTypeLogicDescription
required_tagsstring[]ANDNotes MUST have ALL these tags
any_tagsstring[]ORNotes MUST have AT LEAST ONE of these
excluded_tagsstring[]NOTNotes MUST NOT have ANY of these
required_schemesstring[]IsolationNotes ONLY from these vocabulary schemes
excluded_schemesstring[]ExclusionNotes NOT from these schemes
min_tag_countint-Minimum number of tags required
include_untaggedbool-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:

ParamTypeDescription
scheme_idUUIDFilter by concept scheme
searchstringSearch in labels and definitions
limitintMax 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 /api/v1/concepts/{id}/related

Returns associatively related concepts (not hierarchical).

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 <API_KEY>" \
  -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:

ParamTypeDescription
categorystringFilter 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:

FieldTypeRequiredDescription
namestringYesUnique identifier (lowercase, hyphens)
display_namestringYesHuman-readable name
categorystringYesCategory: code, prose, config, markup, data, api-spec, iac, database, shell, docs, package, observability, legal, communication, research, creative, media, personal, custom
descriptionstringNoDescription of the document type
file_extensionsstring[]NoFile extensions (e.g., [".rs", ".rust"])
filename_patternsstring[]NoExact filename patterns (e.g., ["Cargo.toml"])
content_magicstring[]NoRegex patterns for content detection
chunking_strategystringYessemantic, syntactic, fixed, per_section, whole
syntax_languagestringNoLanguage for syntactic chunking
embedding_model_hintstringNoRecommended 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'\
services:"
}

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

Parameters:

FieldTypeRequiredDescription
filenamestringNoFilename to analyze
contentstringNoContent 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:

MethodConfidenceDescription
filename_pattern1.0Exact pattern match (e.g., `Dockerfile`, `docker-compose.yml`)
extension0.9File extension match (e.g., `.rs` → rust)
content_magic0.7Content pattern recognition (e.g., `openapi:` → OpenAPI)
default0.1Fallback 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:

ParamTypeDescription
include_frontmatterboolInclude YAML frontmatter per note (default: true)
contentstring`original` or `revised` (default: `revised`)

Example:

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

Move Note to Collection

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

{
  "collection_id": "550e8400-..."
}
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 /api/v1/notes/{id}/backlinks

Returns only incoming links to a note.

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:

ParamTypeDescription
limitintMaximum results (default: 10, max: 50)
min_scorefloatMinimum similarity score (default: 0.3)
context_summaryboolInclude 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:

ParamTypeDescription
depthintMaximum traversal depth (default: 2, max: 10)
max_nodesintMaximum nodes to return (default: 50, max: 1000)
min_scorefloatMinimum link score threshold (default: 0.0)
max_edges_per_nodeintMaximum edges returned per node (optional)
edge_filterstringCommunity filter: `all` (default), `intra_community`, `inter_community`
include_structuralboolInclude 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 <API_KEY>"

Graph Diagnostics

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

Returns graph quality diagnostics by sampling embedding pairs.

Query Parameters:

ParamTypeDescription
sample_sizeintNumber 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 <API_KEY>"

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:

FieldTypeRequiredDescription
labelstringYesHuman-readable label for the snapshot
sample_sizeintNoEmbedding pairs to sample (default: 1000)

List Diagnostics Snapshots

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

Returns previously captured diagnostics snapshots.

Query Parameters:

ParamTypeDescription
limitintMax 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:

ParamTypeRequiredDescription
beforeUUIDYesSnapshot ID for the "before" state
afterUUIDYesSnapshot 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:

FieldTypeRequiredDescription
kintNoNumber of nearest neighbors (default: adaptive from graph config)
thresholdfloatNoSNN threshold — edges below this are pruned (default: 0.10)
dry_runboolNoCompute 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:

FieldTypeRequiredDescription
qintNoPFNET q parameter — higher values produce sparser graphs (default: 2, RNG-equivalent)
dry_runboolNoCompute 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:

FieldTypeRequiredDescription
coarse_dimintNoMRL truncation dimension (default: 64, range: 2–768)
similarity_thresholdfloatNoMinimum cosine similarity for edge inclusion (default: 0.3)
resolutionfloatNoLouvain 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:

FieldTypeRequiredDescription
stepsstring[]NoSteps 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 <API_KEY>" \
  -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 <API_KEY>" \
  -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}}\
\
Date: {{date}}\
\
## Attendees\
{{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:

ParamTypeDescription
statusstringFilter by status: `pending`, `processing`, `completed`, `failed`
job_typestringFilter by type: `ai_revision`, `embedding`, etc.
limitintMax 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

[email protected]

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 bounded application-level `core-v1` exports. They include notes, collections, tags, templates, links, and attachment projections while excluding embeddings.

Export Knowledge Shard

GET /api/v1/backup/knowledge-shard?include=notes,links&include_blobs=true

Query Parameters:

ParamTypeDescription
includestringComma-separated `core-v1` components: `notes`, `collections`, `tags`, `templates`, `links`
include_blobsboolOpt in to verified `blobs/<digest>` entries for available stored attachments; defaults to `false`

Imports accept present valid sidecars automatically. A referenced attachment without a sidecar remains a valid reference-only attachment.

Import Knowledge Shard (Multipart Upload)

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

[email protected]

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

[email protected]

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:

ParamTypeDescription
namestringMemory archive name (e.g., `work-notes`)

Response Headers:

  • `Content-Type`: `application/gzip`
  • `Content-Disposition`: metadata-only archive filename, for example `attachment; filename="memory_name_len_10_20260627_120000.sql.gz"`

Example:

curl http://localhost:3000/api/v1/backup/memory/work-notes \
  -H "Authorization: Bearer <API_KEY>" \
  -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

[email protected]

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:

ParamTypeDescription
contentstring`original` or `revised` (default: `revised`)
include_frontmatterboolInclude 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:

HeaderValuesDescription
`X-Fortemi-Memory`Memory nameRoutes 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"
}
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 <ACCESS_TOKEN>

[email protected]

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

Multipart Fields:

FieldTypeRequiredDescription
filebinaryYesImage file (JPEG, PNG, WebP, GIF, etc.)
promptstringNoCustom description prompt
modelstringNoVision 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 <API_KEY>" \
  -F "[email protected]" \
  -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 <ACCESS_TOKEN>

[email protected]

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

Multipart Fields:

FieldTypeRequiredDescription
filebinaryYesAudio file (MP3, WAV, M4A, OGG, FLAC, etc.)
languagestringNoISO 639-1 language hint (e.g., `en`, `es`). Auto-detected if omitted.
modelstringNoWhisper 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 <API_KEY>" \
  -F "[email protected]" \
  -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 <ACCESS_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:

FieldTypeRequiredDescription
`input`stringYesThe user's message
`model`stringNoOllama model slug override (e.g., `qwen3.5:9b`). Uses server default if omitted.
`context`objectNoOptional context for the conversation
`context.note_id`stringNoNote ID for context (future RAG integration)
`context.collection_id`stringNoCollection ID for context (future RAG integration)
`context.search_query`stringNoSearch query for context (future RAG integration)
`context.conversation_history`arrayNoPrevious messages for multi-turn conversation

Each message in `conversation_history`:

FieldTypeRequiredDescription
`role`stringYes`user` or `assistant`
`content`stringYesMessage content
`timestamp`stringNoISO 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:

FieldTypeDescription
`model`stringModel slug used for generation
`context_window`integerTotal context window in tokens
`estimated_available_context`integerContext remaining after system prompt overhead
`max_output_tokens`integerMaximum output tokens per response
`supports_thinking`booleanWhether the model supports chain-of-thought reasoning
`thinking_type`stringOne of: `explicit_tags`, `verbose_reasoning`, `pattern_based`, `none`, `not_tested`
`speed_tok_s`floatEstimated generation speed in tokens/second
`parameter_size`stringModel parameter count (e.g., `8B`, `70B`)
`family`stringModel 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 is RFC 9457 problem+json and sets a `Retry-After` header (the delay is in the header, not a body field):

{
  "type": "https://fortemi.com/problems/service-unavailable",
  "title": "Service Unavailable",
  "status": 503,
  "detail": "Chat service is currently at capacity.",
  "request_id": "018fd1a0-example"
}

Example:

# Simple chat
curl -X POST http://localhost:3000/api/v1/chat \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <API_KEY>" \
  -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 <API_KEY>" \
  -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 <ACCESS_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 <API_KEY>"

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 <API_KEY>"

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 <API_KEY>"

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.

EndpointMethodDescription
`/api/v1/pke/keysets`GETList all keysets
`/api/v1/pke/keysets`POSTCreate a new keyset
`/api/v1/pke/keysets/active`GETGet the currently active keyset
`/api/v1/pke/keysets/import`POSTImport a keyset from JSON
`/api/v1/pke/keysets/{name_or_id}`DELETEDelete a keyset
`/api/v1/pke/keysets/{name_or_id}/active`PUTSet a keyset as active
`/api/v1/pke/keysets/{name_or_id}/export`GETExport 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.

Legacy WebSocket

GET /api/v1/ws

Anonymous-local compatibility WebSocket receiving legacy JSON-encoded `ServerEvent` payloads. It is available only when Fortemi is explicitly running with `REQUIRE_AUTH=false` and `I_UNDERSTAND_NO_AUTH=true`. Auth-required and hosted deployments return `410 Gone`; use `/api/v1/events` for authenticated or tenant/memory-scoped streaming.

Webhooks

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

EndpointMethodDescription
`/api/v1/webhooks`POSTCreate webhook subscription
`/api/v1/webhooks`GETList all webhooks
`/api/v1/webhooks/{id}`GETGet webhook details
`/api/v1/webhooks/{id}`PATCHUpdate webhook
`/api/v1/webhooks/{id}`DELETEDelete webhook
`/api/v1/webhooks/{id}/deliveries`GETList delivery logs
`/api/v1/webhooks/{id}/test`POSTSend test delivery

Create Webhook:

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

{
  "url": "https://example.com/webhook",
  "events": ["NoteUpdated", "JobCompleted", "JobFailed"],
  "secret": "<WEBHOOK_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

Internal hosted credential and inference preview

The internal `hosted-auth` build mounts additional tenant/user-scoped routes; they are intentionally absent from Community Edition and generated public OpenAPI output:

EndpointMethodHosted behavior
`/api/v1/user/secrets`POSTStore a provider credential through the configured KMS provider.
`/api/v1/user/secrets`GETReturn metadata only for the authenticated user.
`/api/v1/user/secrets/{id}`DELETEIdempotently revoke an opaque credential row.
`/api/v1/inference/embed`POSTEmbed with a stored credential and operator-approved provider/model profile.
`/api/v1/inference/catalog`GETReturn caller-available profiles and approved generation/embedding defaults.

Hosted completion, streaming, and embedding reject inline credentials and caller-owned destinations. Responses never include plaintext keys, encrypted envelopes, wrapped keys, or KMS references. See the internal `docs/operations/hosted-user-credentials.md` and `docs/operations/inference-destination-policy.md` runbooks. These routes do not constitute unqualified hosted launch approval.

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 bounded rate-limit and quota readiness metadata. It does not disclose tenant capacity, usage, identities, or Redis connection details.

Response:

{
  "enabled": true,
  "mode": "hosted_shared",
  "shared_state": "ready",
  "shared_state_checked_at": "2026-08-24T20:00:00Z",
  "shared_state_consecutive_failures": 0,
  "policy": { "id": "hosted-api", "version": 1 },
  "identity_dimensions": ["tenant", "principal", "client", "route_class"],
  "tenant_plan_selection": "not_configured",
  "reservation_reconciliation": "foundation_available_not_runtime_configured"
}

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 /livez

Minimal liveness probe for container orchestrators (Kubernetes, Docker Swarm). Returns `200 OK` as long as the process is running, without checking downstream dependencies. `GET /health/live` is a compatibility alias.

Readiness Probe

GET /readyz

Returns `200 OK` only after initialization while required dependencies are available. It returns `503 Service Unavailable` during shutdown drain or when PostgreSQL is unavailable.

Error Responses

All errors follow the RFC 9457 `application/problem+json` format, with a stable `type` URI, plus `title`, `status`, `detail`, and a `request_id`:

{
  "type": "https://fortemi.com/problems/not-found",
  "title": "Not Found",
  "status": 404,
  "detail": "Requested resource is not present or not visible to the caller.",
  "request_id": "018fd1a0-example"
}

See API Error Contract for the full `ProblemType` catalog and the redaction boundary.

Common Problem Types:

StatusType URIDescription
400`https://fortemi.com/problems/validation-error`Invalid request parameters
401`https://fortemi.com/problems/unauthorized`Missing or invalid authentication
403`https://fortemi.com/problems/forbidden`Insufficient permissions
404`https://fortemi.com/problems/not-found`Resource not found
429`https://fortemi.com/problems/rate-limit-exceeded`Too many requests
500`https://fortemi.com/problems/internal-error`Server error

Rate Limiting

Community Edition uses a single process-local limiter across protected routes (there are no per-route tiers). It is configured by environment variables:

  • `RATE_LIMIT_ENABLED`: enable or disable the current process-local limiter
  • `RATE_LIMIT_REQUESTS`: maximum requests per window
  • `RATE_LIMIT_PERIOD_SECS`: window length in seconds

The Community Edition 429 response is `problem+json` (`type=https://fortemi.com/problems/rate-limit-exceeded`) and carries only `Retry-After` with a whole-number delay in seconds. It does not include `X-RateLimit-*`, `RateLimit`, or `RateLimit-Policy` fields. Clients should wait at least that delay before retrying and continue to use bounded backoff.

Internal hosted mode replaces that limiter for authenticated, non-exempt API routes with a Redis-backed fixed-window gate keyed by tenant, principal, client, and route class. Hosted startup and readiness fail closed when Redis is unavailable. Hosted `429` responses include the combined `RateLimit` and `RateLimit-Policy` draft fields plus `Retry-After` when a retry time is known; quota-state failure returns `503`. Those fields are not Community Edition behavior, and neither profile emits legacy `X-RateLimit-*` compatibility headers. Tenant-plan selection, billing limits, and non-request producer integration remain unavailable; see ADR-098 and #714.

Versioning

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

See Also