Fortémi provides a RESTful API for AI-enhanced note management with semantic search capabilities.
Base URL: http://localhost:3000
OpenAPI Spec: openapi.yaml
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) |
For trusted integrations, use API key authentication:
curl -H "Authorization: Bearer mm_key_xxx" \
http://localhost:3000/api/v1/notesAPI 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}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 /api/v1/notes/{id}Returns the full note with original and revised content, tags, and semantic links.
PATCH /api/v1/notes/{id}
Content-Type: application/json
{
"content": "Updated content...",
"starred": true,
"archived": false
}Quick endpoint for status-only updates:
PATCH /api/v1/notes/{id}/status
Content-Type: application/json
{
"starred": true,
"archived": false
}DELETE /api/v1/notes/{id}Soft-deletes the note. Can be restored later.
POST /api/v1/notes/{id}/restoreRestores a soft-deleted note.
POST /api/v1/notes/{id}/purgePermanently deletes a note and all associated data.
POST /api/v1/notes/{id}/reprocessQueues a note for AI reprocessing (re-embedding, re-revision).
GET /api/v1/notes?limit=50&offset=0&filter=starredQuery 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 |
POST /api/v1/notes/bulk
Content-Type: application/json
{
"notes": [
{
"content": "# Note 1",
"tags": ["batch"]
},
{
"content": "# Note 2",
"tags": ["batch"]
}
]
}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"]}'Fortémi maintains dual-track versioning: original (user-written) and revised (AI-enhanced) histories.
GET /api/v1/notes/{id}/versionsReturns 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 /api/v1/notes/{id}/versions/{version}Returns the full content of a specific version.
POST /api/v1/notes/{id}/versions/{version}/restoreRestores a note to a previous version, creating a new version in the process.
DELETE /api/v1/notes/{id}/versions/{version}Deletes a specific version (cannot delete current version).
GET /api/v1/notes/{id}/versions/diff?from=2&to=3Returns 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"
}GET /api/v1/notes/{id}/provenanceReturns 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"
}
]
}POST /api/v1/notes/{id}/attachments
Content-Type: multipart/form-data
Authorization: Bearer <token>
file=@photo.jpgUpload 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"POST /api/v1/notes/{id}/attachments/upload
Content-Type: multipart/form-data
Authorization: Bearer <token>
file=@photo.jpgAlternative 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"GET /api/v1/notes/{id}/attachmentsReturns 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 /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"GET /api/v1/attachments/{id}/downloadDownloads 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 /api/v1/attachments/{id}/metadataReturns 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 /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 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.
GET /api/v1/memories/searchA 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 /api/v1/notes/{id}/memory-provenanceReturns 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"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
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
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.
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.
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"
}GET /api/v1/notes/{id}/fullReconstructs 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
}Fortémi uses UUIDv7 for temporal ordering.
GET /api/v1/notes/timeline?limit=50&before=2026-01-24T12:00:00ZReturns 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 |
GET /api/v1/notes/activity?days=7Returns 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
}
]
}Monitor the health and quality of your knowledge base.
GET /api/v1/health/knowledgeReturns 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"
}GET /api/v1/health/orphan-tagsLists tags that are defined but not used by any notes.
GET /api/v1/health/stale-notes?days=180Returns notes that haven't been updated in N days.
GET /api/v1/health/unlinked-notesReturns notes with no semantic links to other notes.
GET /api/v1/health/tag-cooccurrence?min_count=5Returns 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=20Query 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)
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"]
GET /api/v1/search?query=api&tag:backend&created_after:2026-01-01Filter syntax in query string (soft filtering, combined with fuzzy search):
tag:name- Filter by tagcollection:uuid- Filter by collectioncreated_after:ISO8601- Date rangecreated_before:ISO8601- Date range
GET /api/v1/tagsReturns all tags with usage counts.
GET /api/v1/notes/{id}/tagsReturns all tags applied to a specific note.
PUT /api/v1/notes/{id}/tags
Content-Type: application/json
{
"tags": ["updated", "tags"]
}Replaces all tags for a note.
Fortémi implements W3C SKOS (Simple Knowledge Organization System) for controlled vocabularies and semantic tagging.
Concept schemes are top-level vocabularies that organize related concepts.
GET /api/v1/concepts/schemesPOST /api/v1/concepts/schemes
Content-Type: application/json
{
"title": "Project Taxonomy",
"description": "Controlled vocabulary for project classification",
"namespace": "https://example.org/projects/"
}GET /api/v1/concepts/schemes/{id}PATCH /api/v1/concepts/schemes/{id}
Content-Type: application/json
{
"title": "Updated Project Taxonomy",
"description": "Updated description"
}GET /api/v1/concepts/schemes/{id}/top-conceptsReturns the top-level concepts in a scheme (concepts with no broader concepts).
GET /api/v1/concepts?scheme_id={scheme_id}&search=machineQuery Parameters:
| Param | Type | Description |
|---|---|---|
| scheme_id | UUID | Filter by concept scheme |
| search | string | Search in labels and definitions |
| limit | int | Max results |
GET /api/v1/concepts/autocomplete?q=mach&scheme_id={scheme_id}Fast autocomplete endpoint for UI type-ahead.
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 /api/v1/concepts/{id}GET /api/v1/concepts/{id}/fullReturns concept with all relationships (broader, narrower, related) and usage statistics.
PATCH /api/v1/concepts/{id}
Content-Type: application/json
{
"pref_label": "Machine Learning (Updated)",
"definition": "Updated definition"
}DELETE /api/v1/concepts/{id}Deletes a concept. Fails if the concept is in use by notes.
GET /api/v1/concepts/{id}/ancestorsReturns all ancestor concepts in the hierarchy.
GET /api/v1/concepts/{id}/descendants?depth=2Returns all descendant concepts up to a specified depth.
GET /api/v1/concepts/{id}/broaderReturns immediate parent concepts.
POST /api/v1/concepts/{id}/broader
Content-Type: application/json
{
"broader_id": "550e8400-..."
}Establishes a broader/narrower relationship.
GET /api/v1/concepts/{id}/narrowerReturns immediate child concepts.
POST /api/v1/concepts/{id}/narrower
Content-Type: application/json
{
"narrower_id": "550e8400-..."
}GET /api/v1/concepts/{id}/relatedReturns associatively related concepts (not hierarchical).
POST /api/v1/concepts/{id}/related
Content-Type: application/json
{
"related_id": "550e8400-..."
}GET /api/v1/notes/{id}/conceptsReturns all SKOS concepts applied to a note.
POST /api/v1/notes/{id}/concepts
Content-Type: application/json
{
"concept_id": "550e8400-..."
}DELETE /api/v1/notes/{id}/concepts/{concept_id}GET /api/v1/concepts/governanceReturns 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
}GET /api/v1/concepts/schemes/{id}/export/turtleExports a concept scheme in RDF Turtle format (W3C SKOS-compatible).
GET /api/v1/concepts/schemes/export/turtleExports 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.ttlSKOS Collections group related concepts for convenience (W3C SKOS Section 9).
GET /api/v1/concepts/collections?scheme_id={scheme_id}POST /api/v1/concepts/collections
Content-Type: application/json
{
"scheme_id": "550e8400-...",
"label": "Core ML Concepts",
"description": "Essential machine learning concepts"
}GET /api/v1/concepts/collections/{id}PATCH /api/v1/concepts/collections/{id}
Content-Type: application/json
{
"label": "Updated Collection Name"
}DELETE /api/v1/concepts/collections/{id}PUT /api/v1/concepts/collections/{id}/members
Content-Type: application/json
{
"concept_ids": ["550e8400-...", "660e8400-..."]
}Replaces all members of a collection.
POST /api/v1/concepts/collections/{id}/members/{concept_id}DELETE /api/v1/concepts/collections/{id}/members/{concept_id}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 /api/v1/document-types/:nameReturns 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"
}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,
...
}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 /api/v1/document-types/:nameDeletes a custom document type. System types cannot be deleted.
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 |
Note collections organize notes into folders with hierarchy support.
GET /api/v1/collections?parent_id=<uuid>POST /api/v1/collections
Content-Type: application/json
{
"name": "Work Projects",
"description": "Work-related notes",
"parent_id": null
}GET /api/v1/collections/{id}PATCH /api/v1/collections/{id}
Content-Type: application/json
{
"name": "Updated Collection Name",
"description": "Updated description"
}DELETE /api/v1/collections/{id}GET /api/v1/collections/{id}/notesReturns all notes in a collection.
GET /api/v1/collections/{id}/export?include_frontmatter=true&content=revisedExports 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.mdPOST /api/v1/notes/{note_id}/move
Content-Type: application/json
{
"collection_id": "550e8400-..."
}GET /api/v1/notes/{id}/linksReturns 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}/backlinksReturns only incoming links to a note.
GET /api/v1/notes/{id}/related?limit=10&min_score=0.3&context_summary=trueDiscovers 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.
GET /api/v1/graph/{id}?depth=2&max_nodes=50Traverses 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
}
]
}GET /api/v1/graph/topology/statsReturns 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"GET /api/v1/graph/diagnostics?sample_size=1000Returns 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"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) |
GET /api/v1/graph/diagnostics/history?limit=20Returns previously captured diagnostics snapshots.
Query Parameters:
| Param | Type | Description |
|---|---|---|
| limit | int | Max snapshots to return (default: 20, max: 100) |
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 |
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) |
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) |
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) |
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 allow creating isolated embedding spaces for multi-tenant or specialized use cases.
GET /api/v1/embedding-setsPOST /api/v1/embedding-sets
Content-Type: application/json
{
"slug": "client-acme",
"name": "ACME Corp Knowledge",
"embedding_config_id": "550e8400-..."
}GET /api/v1/embedding-sets/{slug}PATCH /api/v1/embedding-sets/{slug}
Content-Type: application/json
{
"name": "Updated Name"
}DELETE /api/v1/embedding-sets/{slug}GET /api/v1/embedding-sets/{slug}/membersReturns all notes in an embedding set.
POST /api/v1/embedding-sets/{slug}/members
Content-Type: application/json
{
"note_ids": ["550e8400-...", "660e8400-..."]
}DELETE /api/v1/embedding-sets/{slug}/members/{note_id}POST /api/v1/embedding-sets/{slug}/refreshRegenerates embeddings for all notes in the set.
GET /api/v1/embedding-configsReturns available embedding model configurations.
GET /api/v1/embedding-configs/defaultGET /api/v1/embedding-configs/{id}Returns details for a specific embedding configuration.
POST /api/v1/embedding-configs
Content-Type: application/json
{
"name": "Custom Config",
"model": "mxbai-embed-large",
"dimension": 1024,
"provider": "ollama",
"is_default": false
}PATCH /api/v1/embedding-configs/{id}
Content-Type: application/json
{
"name": "Updated Config",
"is_default": true
}DELETE /api/v1/embedding-configs/{id}Deletes a non-default embedding configuration.
GET /api/v1/templatesPOST /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 /api/v1/templates/{id}PATCH /api/v1/templates/{id}
Content-Type: application/json
{
"name": "Updated Template Name",
"content": "Updated template content"
}DELETE /api/v1/templates/{id}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.
Background processing status for AI operations.
GET /api/v1/jobs?status=pending&job_type=ai_revisionQuery 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 |
POST /api/v1/jobs
Content-Type: application/json
{
"job_type": "ai_revision",
"target_id": "550e8400-...",
"parameters": {
"mode": "full"
}
}GET /api/v1/jobs/{id}GET /api/v1/jobs/pendingReturns count of pending jobs.
GET /api/v1/jobs/statsReturns queue health metrics:
{
"pending": 5,
"processing": 2,
"completed_last_hour": 150,
"failed_last_hour": 0,
"avg_processing_time_ms": 2341
}Pause and resume job processing globally or per-archive.
GET /api/v1/jobs/statusReturns the current pause state and queue statistics:
{
"global": "running",
"archives": {
"research": "paused"
},
"queue": {
"pending": 42,
"running": 3
}
}POST /api/v1/jobs/pausePauses job processing globally. Jobs already running will complete, but no new jobs will be picked up.
POST /api/v1/jobs/resumeResumes globally paused job processing.
POST /api/v1/jobs/pause/{archive}Pauses job processing for a specific memory archive. Jobs for other archives continue normally.
POST /api/v1/jobs/resume/{archive}Resumes job processing for a specific memory archive.
Fortémi provides multiple backup strategies for different use cases.
GET /api/v1/backup/exportExports all notes and metadata as JSON.
GET /api/v1/backup/downloadDownloads the most recent export as a file.
POST /api/v1/backup/import
Content-Type: multipart/form-data
file=@backup.jsonImports notes from a JSON export.
POST /api/v1/backup/triggerManually triggers a backup job.
GET /api/v1/backup/statusReturns status of the most recent backup operation.
Knowledge shards are application-level exports that include notes, concepts, and metadata but exclude embeddings.
GET /api/v1/backup/knowledge-shard?format=jsonQuery Parameters:
| Param | Type | Description |
|---|---|---|
| format | string | Export format: json or yaml |
| include_deleted | bool | Include soft-deleted notes |
POST /api/v1/backup/knowledge-shard/upload?on_conflict=skip
Content-Type: multipart/form-data
file=@backup.shardQuery Parameters: on_conflict (skip/replace/merge), dry_run (bool), include (csv), skip_embedding_regen (bool)
POST /api/v1/backup/knowledge-shard/import
Content-Type: application/json
{"shard_base64": "...", "on_conflict": "skip"}Full PostgreSQL backups including embeddings and all data.
GET /api/v1/backup/databaseDownloads a full pg_dump of the database.
POST /api/v1/backup/database/snapshot
Content-Type: application/json
{
"label": "pre-migration-backup"
}Creates a named database snapshot.
POST /api/v1/backup/database/upload
Content-Type: multipart/form-data
file=@backup.sqlUploads a database backup file for later restoration.
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.
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/gzipContent-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.gzKnowledge archives bundle a knowledge shard with metadata in a single .archive file.
GET /api/v1/backup/knowledge-archive/{filename}POST /api/v1/backup/knowledge-archive
Content-Type: multipart/form-data
file=@knowledge-archive.archiveGET /api/v1/backup/listReturns 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 /api/v1/backup/list/{filename}Returns detailed information about a specific backup file.
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).
GET /api/v1/backup/metadata/{filename}Returns metadata for a backup file.
PUT /api/v1/backup/metadata/{filename}
Content-Type: application/json
{
"label": "Updated label",
"description": "Updated description",
"tags": ["important", "pre-migration"]
}GET /api/v1/notes/{id}/export?content=revised&include_frontmatter=trueReturns 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) |
Fortemi supports parallel memory archives for organizing different knowledge domains with full schema-level isolation.
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") |
GET /api/v1/memoriesReturns all memory archives with metadata (name, description, note count, size, schema version).
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 /api/v1/memories/:namePATCH /api/v1/memories/:name
Content-Type: application/json
{
"description": "Updated description"
}DELETE /api/v1/memories/:namePermanently deletes the memory schema and all data. Cannot be undone.
POST /api/v1/archives/:name/defaultSets the specified memory as default. Only one memory can be default at a time.
GET /api/v1/archives/:name/statsReturns note count and storage size for a specific memory.
GET /api/v1/memories/overviewReturns 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"
}
}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.
Ad-hoc image description using the configured vision LLM. Requires OLLAMA_VISION_MODEL to be set.
POST /api/v1/vision/describe
Content-Type: multipart/form-data
Authorization: Bearer <token>
file=@image.jpgAnalyzes 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 file503 Service Unavailable:OLLAMA_VISION_MODELnot 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"Ad-hoc audio transcription using a Whisper-compatible backend. Requires WHISPER_BASE_URL to be set.
POST /api/v1/audio/transcribe
Content-Type: multipart/form-data
Authorization: Bearer <token>
file=@recording.mp3Transcribes 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 file503 Service Unavailable:WHISPER_BASE_URLnot 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"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.
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 input503 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."}
]
}
}'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"GET /api/v1/modelsReturns 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"GET /api/v1/extraction/statsReturns 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"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.
POST /api/v1/pke/keygenGenerates a new PKE key pair. Returns the public key and a secure private key identifier.
POST /api/v1/pke/address
Content-Type: application/json
{
"public_key": "..."
}Derives a shareable address from a public key.
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.
POST /api/v1/pke/decrypt
Content-Type: application/json
{
"ciphertext": "...",
"private_key_id": "key_xxx"
}Decrypts ciphertext using the specified private key.
POST /api/v1/pke/recipients
Content-Type: application/json
{
"note_id": "550e8400-..."
}Returns the list of addresses that can decrypt a note.
GET /api/v1/pke/verify/{address}Verifies that an address is valid and corresponds to a registered public key.
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 |
Fortémi provides real-time event streaming through three channels. For comprehensive documentation, see Real-Time Events.
GET /api/v1/eventsStreams all server events as text/event-stream. Each event includes an event: type field and data: JSON payload. Keep-alive sent every 15 seconds.
GET /api/v1/wsFull-duplex WebSocket connection receiving JSON-encoded events. Send "refresh" to trigger an immediate QueueStatus response.
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.
GET /api/v1/memory/infoReturns system memory usage information.
Response:
{
"total_bytes": 16777216000,
"used_bytes": 8388608000,
"available_bytes": 8388608000,
"percent_used": 50.0
}GET /api/v1/rate-limit/statusReturns current rate limit status for the authenticated client.
Response:
{
"limit": 100,
"remaining": 87,
"reset_at": "2026-01-24T12:01:00Z",
"retry_after_seconds": 45
}GET /healthReturns 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 startupavailable: Whether at least one GPU semaphore permit is free (i.e., chat is not at capacity)max_concurrent: TheCHAT_MAX_CONCURRENTsetting
GET /health/liveMinimal liveness probe for container orchestrators (Kubernetes, Docker Swarm). Returns 200 OK as long as the process is running, without checking downstream dependencies.
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 |
- Default: 100 requests/minute per API key
- Search: 30 requests/minute
- AI operations: 10 requests/minute
Rate limit headers:
X-RateLimit-Limit: Request limitX-RateLimit-Remaining: Remaining requestsX-RateLimit-Reset: Reset timestamp
The API is versioned via URL path (/api/v1/). Breaking changes will increment the version number.
- MCP Server Documentation - Claude integration
- Multi-Memory Guide - Parallel memory archives and federated search
- Real-Time Events - SSE, WebSocket, and webhook event streaming
- Authentication Guide - OAuth2 flows
- Integration Guide - Client examples