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
OAuth2 (Recommended)
The API supports full OAuth2 with Dynamic Client Registration (RFC 7591).
# 1. Discover endpoints
curl http://localhost:3000/.well-known/oauth-authorization-server
# 2. Register client
curl -X POST http://localhost:3000/oauth/register \
-H "Content-Type: application/json" \
-d '{"client_name": "My App", "grant_types": ["client_credentials"]}'
# 3. Get token
curl -X POST http://localhost:3000/oauth/token \
-d "grant_type=client_credentials&client_id=xxx&client_secret=yyy"
OAuth2 Endpoints:
| Endpoint | Method | Description |
|---|---|---|
| `/.well-known/oauth-authorization-server` | GET | OAuth2 discovery metadata |
| `/.well-known/oauth-protected-resource` | GET | Protected resource metadata |
| `/oauth/authorize` | GET, POST | Authorization endpoint |
| `/oauth/register` | POST | Dynamic client registration (RFC 7591) |
| `/oauth/token` | POST | Token endpoint |
| `/oauth/introspect` | POST | Token introspection (RFC 7662) |
| `/oauth/revoke` | POST | Token revocation (RFC 7009) |
API Keys (Simple)
For trusted integrations, use API key authentication:
curl -H "Authorization: Bearer <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:
| 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\
\
...",
"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:
| Param | Type | Description |
|---|---|---|
| limit | int | Max results (default: 50) |
| offset | int | Pagination offset |
| filter | string | `starred` or `archived` |
| tags | string | Comma-separated tag filter |
| created_after | ISO8601 | Date filter |
| created_before | ISO8601 | Date filter |
Bulk Create Notes
POST /api/v1/notes/bulk
Content-Type: application/json
{
"notes": [
{
"content": "# Note 1",
"tags": ["batch"]
},
{
"content": "# Note 2",
"tags": ["batch"]
}
]
}
Bulk Reprocess Notes
POST /api/v1/notes/reprocess
Content-Type: application/json
{
"limit": 500,
"revision_mode": "light",
"steps": ["embedding", "linking", "title_generation"],
"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 | Input note limit (default: 500, capped at 5000; zero or negative queues no work) |
| revision_mode | string | No | `full`, `light` (default), or `none` |
| steps | string[] | No | Steps 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_ids | UUID[] | No | Specific 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
Memory search enables temporal-spatial queries on file attachments based on when and where they were captured. Uses a single unified endpoint with parameter-based mode selection.
For comprehensive documentation, see Memory Search Guide.
Search Memories
GET /api/v1/memories/search
A single endpoint that switches between location, temporal, and combined modes based on which query parameters are provided.
Query Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
| `lat` | float | Conditional | Latitude in decimal degrees (-90 to 90). Required for location/combined mode. |
| `lon` | float | Conditional | Longitude in decimal degrees (-180 to 180). Required for location/combined mode. |
| `radius` | float | No | Search radius in meters (default: 1000) |
| `start` | datetime | Conditional | Start of time range (ISO 8601 or flexible format). Required for time/combined mode. |
| `end` | datetime | Conditional | End of time range (ISO 8601 or flexible format). Required for time/combined mode. |
At least one search dimension is required: `lat`+`lon` for location, `start`+`end` for temporal, or all five for combined.
Mode Selection:
| Parameters Provided | Mode | Description |
|---|---|---|
| `lat` + `lon` (+ optional `radius`) | `location` | Spatial search, nearest memories |
| `start` + `end` | `time` | Temporal search, memories in time range |
| All five | `combined` | Intersection of spatial + temporal |
| None | 400 error | At least one dimension required |
Response:
{
"mode": "location",
"results": [
{
"provenance_id": "uuid",
"attachment_id": "uuid",
"note_id": "uuid",
"filename": "IMG_1234.jpg",
"content_type": "image/jpeg",
"distance_m": 245.7,
"capture_time_start": "2026-01-15T14:30:00Z",
"capture_time_end": "2026-01-15T14:30:00Z",
"location_name": "Eiffel Tower",
"event_type": "photo"
}
],
"count": 1
}
Examples:
# Location search: memories within 1km of a point
curl "http://localhost:3000/api/v1/memories/search?lat=37.7749&lon=-122.4194&radius=1000" \
-H "Authorization: Bearer <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:
| Param | Type | Description |
|---|---|---|
| limit | int | Max results (default: 50) |
| before | ISO8601 | Notes created before this time |
| after | ISO8601 | Notes created after this time |
Activity View
GET /api/v1/notes/activity?days=7
Returns note activity statistics over a time period.
Response:
{
"period_days": 7,
"notes_created": 42,
"notes_updated": 18,
"notes_deleted": 3,
"daily_breakdown": [
{
"date": "2026-01-24",
"created": 8,
"updated": 4,
"deleted": 1
}
]
}
Knowledge Health Dashboard
Monitor the health and quality of your knowledge base.
Overall Knowledge Health
GET /api/v1/health/knowledge
Returns comprehensive knowledge base health metrics.
Response:
{
"total_notes": 1523,
"orphan_notes": 42,
"stale_notes": 18,
"unlinked_notes": 95,
"avg_links_per_note": 3.2,
"tag_coverage": 0.87,
"last_activity": "2026-01-24T15:30:00Z"
}
Orphan Tags
GET /api/v1/health/orphan-tags
Lists tags that are defined but not used by any notes.
Stale Notes
GET /api/v1/health/stale-notes?days=180
Returns notes that haven't been updated in N days.
Unlinked Notes
GET /api/v1/health/unlinked-notes
Returns notes with no semantic links to other notes.
Tag Co-occurrence
GET /api/v1/health/tag-cooccurrence?min_count=5
Returns tag co-occurrence statistics for discovering tag relationships.
Response:
{
"pairs": [
{
"tag_a": "machine-learning",
"tag_b": "python",
"count": 42,
"correlation": 0.78
}
]
}
Search
Hybrid Search
GET /api/v1/search?query=machine+learning&mode=hybrid&limit=20
Query Parameters:
| Param | Type | Description |
|---|---|---|
| query | string | Search query (required) |
| mode | string | `hybrid` (default), `fts`, or `semantic` |
| limit | int | Max results (default: 20) |
| strict_filter | object | Strict tag filter (see below) |
Response:
{
"results": [
{
"note_id": "550e8400-...",
"score": 0.85,
"snippet": "...machine learning algorithms...",
"title": "ML Research Notes",
"tags": ["ml", "research"]
}
],
"total": 42
}
Search Modes:
- `hybrid`: Combines FTS + semantic (best for most queries)
- `fts`: Full-text search only (exact keyword matching)
- `semantic`: Vector similarity only (conceptual matching)
Strict Tag Filtering
Apply guaranteed tag-based filtering before fuzzy search. Unlike query string filters, strict filters guarantee exact matches.
GET /api/v1/search?q=authentication&strict_filter=<json>
Pass the `strict_filter` parameter as a URL-encoded JSON string:
curl "http://localhost:3000/api/v1/search?q=authentication" \
-H "Authorization: Bearer <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:
| Field | Type | Logic | Description |
|---|---|---|---|
| required_tags | string[] | AND | Notes MUST have ALL these tags |
| any_tags | string[] | OR | Notes MUST have AT LEAST ONE of these |
| excluded_tags | string[] | NOT | Notes MUST NOT have ANY of these |
| required_schemes | string[] | Isolation | Notes ONLY from these vocabulary schemes |
| excluded_schemes | string[] | Exclusion | Notes NOT from these schemes |
| min_tag_count | int | - | Minimum number of tags required |
| include_untagged | bool | - | Include notes with no tags (default: true) |
Use Cases:
- Client isolation: `"required_schemes": ["client-acme"]`
- Project search: `"required_tags": ["project:matric"]`
- Priority filter: `"any_tags": ["priority:high", "priority:critical"]`
- Exclude drafts: `"excluded_tags": ["draft", "wip", "internal"]`
Advanced Filters (Query String)
GET /api/v1/search?query=api&tag:backend&created_after:2026-01-01
Filter syntax in query string (soft filtering, combined with fuzzy search):
- `tag:name` - Filter by tag
- `collection:uuid` - Filter by collection
- `created_after:ISO8601` - Date range
- `created_before:ISO8601` - Date range
Tags
List Tags
GET /api/v1/tags
Returns all tags with usage counts.
Get Note Tags
GET /api/v1/notes/{id}/tags
Returns all tags applied to a specific note.
Set Note Tags
PUT /api/v1/notes/{id}/tags
Content-Type: application/json
{
"tags": ["updated", "tags"]
}
Replaces all tags for a note.
SKOS Concepts
Fortémi implements W3C SKOS (Simple Knowledge Organization System) for controlled vocabularies and semantic tagging.
Concept Schemes
Concept schemes are top-level vocabularies that organize related concepts.
List Concept Schemes
GET /api/v1/concepts/schemes
Create Concept Scheme
POST /api/v1/concepts/schemes
Content-Type: application/json
{
"title": "Project Taxonomy",
"description": "Controlled vocabulary for project classification",
"namespace": "https://example.org/projects/"
}
Get Concept Scheme
GET /api/v1/concepts/schemes/{id}
Update Concept Scheme
PATCH /api/v1/concepts/schemes/{id}
Content-Type: application/json
{
"title": "Updated Project Taxonomy",
"description": "Updated description"
}
Get Top Concepts
GET /api/v1/concepts/schemes/{id}/top-concepts
Returns the top-level concepts in a scheme (concepts with no broader concepts).
Concepts
List/Search Concepts
GET /api/v1/concepts?scheme_id={scheme_id}&search=machine
Query Parameters:
| Param | Type | Description |
|---|---|---|
| scheme_id | UUID | Filter by concept scheme |
| search | string | Search in labels and definitions |
| limit | int | Max results |
Autocomplete Concepts
GET /api/v1/concepts/autocomplete?q=mach&scheme_id={scheme_id}
Fast autocomplete endpoint for UI type-ahead.
Create Concept
POST /api/v1/concepts
Content-Type: application/json
{
"scheme_id": "550e8400-...",
"pref_label": "Machine Learning",
"alt_labels": ["ML", "Statistical Learning"],
"definition": "A field of AI focused on learning from data",
"notation": "ML-001"
}
Get Concept
GET /api/v1/concepts/{id}
Get Full Concept
GET /api/v1/concepts/{id}/full
Returns concept with all relationships (broader, narrower, related) and usage statistics.
Update Concept
PATCH /api/v1/concepts/{id}
Content-Type: application/json
{
"pref_label": "Machine Learning (Updated)",
"definition": "Updated definition"
}
Delete Concept
DELETE /api/v1/concepts/{id}
Deletes a concept. Fails if the concept is in use by notes.
Concept Relationships
Get Ancestors
GET /api/v1/concepts/{id}/ancestors
Returns all ancestor concepts in the hierarchy.
Get Descendants
GET /api/v1/concepts/{id}/descendants?depth=2
Returns all descendant concepts up to a specified depth.
Get Broader Concepts
GET /api/v1/concepts/{id}/broader
Returns immediate parent concepts.
Add Broader Concept
POST /api/v1/concepts/{id}/broader
Content-Type: application/json
{
"broader_id": "550e8400-..."
}
Establishes a broader/narrower relationship.
Get Narrower Concepts
GET /api/v1/concepts/{id}/narrower
Returns immediate child concepts.
Add Narrower Concept
POST /api/v1/concepts/{id}/narrower
Content-Type: application/json
{
"narrower_id": "550e8400-..."
}
Get Related Concepts
GET /api/v1/concepts/{id}/related
Returns associatively related concepts (not hierarchical).
Add Related Concept
POST /api/v1/concepts/{id}/related
Content-Type: application/json
{
"related_id": "550e8400-..."
}
Note Tagging with Concepts
Get Note Concepts
GET /api/v1/notes/{id}/concepts
Returns all SKOS concepts applied to a note.
Tag Note with Concept
POST /api/v1/notes/{id}/concepts
Content-Type: application/json
{
"concept_id": "550e8400-..."
}
Untag Note Concept
DELETE /api/v1/notes/{id}/concepts/{concept_id}
Governance
Get Governance Stats
GET /api/v1/concepts/governance
Returns governance and quality metrics for the concept system.
Response:
{
"total_schemes": 5,
"total_concepts": 342,
"concepts_with_definitions": 298,
"concepts_in_use": 215,
"avg_hierarchy_depth": 3.2,
"orphan_concepts": 12
}
Export
Export Scheme as Turtle
GET /api/v1/concepts/schemes/{id}/export/turtle
Exports a concept scheme in RDF Turtle format (W3C SKOS-compatible).
Export All Schemes as Turtle
GET /api/v1/concepts/schemes/export/turtle
Exports all concept schemes in a single RDF Turtle document.
Example:
curl http://localhost:3000/api/v1/concepts/schemes/export/turtle \
-H "Authorization: Bearer <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:
| Param | Type | Description |
|---|---|---|
| category | string | Filter by category (code, prose, config, markup, data, api-spec, iac, etc.) |
Response:
{
"document_types": [
{
"name": "rust",
"display_name": "Rust",
"category": "code",
"file_extensions": [".rs"],
"filename_patterns": ["Cargo.toml", "Cargo.lock"],
"chunking_strategy": "syntactic",
"is_system": true
}
]
}
Get Document Type
GET /api/v1/document-types/:name
Returns details for a specific document type.
Response:
{
"name": "rust",
"display_name": "Rust",
"category": "code",
"description": "Rust programming language",
"file_extensions": [".rs"],
"filename_patterns": ["Cargo.toml", "Cargo.lock"],
"content_magic": [],
"chunking_strategy": "syntactic",
"syntax_language": "rust",
"embedding_model_hint": null,
"is_system": true,
"created_at": "2026-01-15T10:00:00Z"
}
Create Document Type
POST /api/v1/document-types
Content-Type: application/json
{
"name": "my-custom-type",
"display_name": "My Custom Type",
"category": "custom",
"description": "Custom document type for specialized content",
"file_extensions": [".mytype"],
"filename_patterns": ["*.mytype"],
"content_magic": ["^MYTYPE:"],
"chunking_strategy": "semantic",
"syntax_language": null,
"embedding_model_hint": null
}
Creates a custom document type.
Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Unique identifier (lowercase, hyphens) |
| display_name | string | Yes | Human-readable name |
| category | string | Yes | Category: code, prose, config, markup, data, api-spec, iac, database, shell, docs, package, observability, legal, communication, research, creative, media, personal, custom |
| description | string | No | Description of the document type |
| file_extensions | string[] | No | File extensions (e.g., [".rs", ".rust"]) |
| filename_patterns | string[] | No | Exact filename patterns (e.g., ["Cargo.toml"]) |
| content_magic | string[] | No | Regex patterns for content detection |
| chunking_strategy | string | Yes | semantic, syntactic, fixed, per_section, whole |
| syntax_language | string | No | Language for syntactic chunking |
| embedding_model_hint | string | No | Recommended embedding model |
Response (201 Created):
{
"name": "my-custom-type",
"display_name": "My Custom Type",
"category": "custom",
"is_system": false,
...
}
Update Document Type
PATCH /api/v1/document-types/:name
Content-Type: application/json
{
"display_name": "Updated Display Name",
"description": "Updated description",
"file_extensions": [".mytype", ".mt"]
}
Updates a custom document type. System types cannot be updated.
Delete Document Type
DELETE /api/v1/document-types/:name
Deletes a custom document type. System types cannot be deleted.
Detect Document Type
POST /api/v1/document-types/detect
Content-Type: application/json
{
"filename": "docker-compose.yml",
"content": "version: '3.8'\
services:"
}
Auto-detects document type from filename and/or content.
Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
| filename | string | No | Filename to analyze |
| content | string | No | Content to analyze (first 1KB sufficient) |
At least one of filename or content must be provided.
Response:
{
"document_type": "docker-compose",
"confidence": 0.9,
"detection_method": "filename_pattern",
"category": "iac",
"chunking_strategy": "per_section",
"alternatives": [
{
"document_type": "yaml",
"confidence": 0.5,
"detection_method": "extension"
}
]
}
Detection Methods:
| Method | Confidence | Description |
|---|---|---|
| filename_pattern | 1.0 | Exact pattern match (e.g., `Dockerfile`, `docker-compose.yml`) |
| extension | 0.9 | File extension match (e.g., `.rs` → rust) |
| content_magic | 0.7 | Content pattern recognition (e.g., `openapi:` → OpenAPI) |
| default | 0.1 | Fallback to generic type |
Collections
Note collections organize notes into folders with hierarchy support.
List Collections
GET /api/v1/collections?parent_id=<uuid>
Create Collection
POST /api/v1/collections
Content-Type: application/json
{
"name": "Work Projects",
"description": "Work-related notes",
"parent_id": null
}
Get Collection
GET /api/v1/collections/{id}
Update Collection
PATCH /api/v1/collections/{id}
Content-Type: application/json
{
"name": "Updated Collection Name",
"description": "Updated description"
}
Delete Collection
DELETE /api/v1/collections/{id}
Get Collection Notes
GET /api/v1/collections/{id}/notes
Returns all notes in a collection.
Export Collection as Markdown
GET /api/v1/collections/{id}/export?include_frontmatter=true&content=revised
Exports all notes in a collection as a single concatenated Markdown document with optional YAML frontmatter separators.
Query Parameters:
| Param | Type | Description |
|---|---|---|
| include_frontmatter | bool | Include YAML frontmatter per note (default: true) |
| content | string | `original` or `revised` (default: `revised`) |
Example:
curl "http://localhost:3000/api/v1/collections/550e8400-e29b-41d4-a716-446655440000/export" \
-H "Authorization: Bearer <API_KEY>" \
-o collection-export.md
Move Note to Collection
POST /api/v1/notes/{note_id}/move
Content-Type: application/json
{
"collection_id": "550e8400-..."
}
Links
Get Note Links
GET /api/v1/notes/{id}/links
Returns bidirectional semantic links:
{
"outgoing": [
{"to_note_id": "...", "score": 0.82, "kind": "semantic"}
],
"incoming": [
{"from_note_id": "...", "score": 0.78, "kind": "semantic"}
]
}
Get Note Backlinks
GET /api/v1/notes/{id}/backlinks
Returns only incoming links to a note.
Get Related Notes
GET /api/v1/notes/{id}/related?limit=10&min_score=0.3&context_summary=true
Discovers related notes by combining semantic similarity (vector search on embeddings) with direct graph links. Returns a unified, deduplicated list with an optional LLM-generated context summary explaining the thematic connection.
Query Parameters:
| Param | Type | Description |
|---|---|---|
| limit | int | Maximum results (default: 10, max: 50) |
| min_score | float | Minimum similarity score (default: 0.3) |
| context_summary | bool | Include LLM context summary (default: false) |
Response:
{
"note_id": "...",
"related": [
{
"note_id": "...",
"score": 0.92,
"snippet": "Related content...",
"title": "Note Title",
"tags": ["topic-a"],
"source": "semantic"
},
{
"note_id": "...",
"score": 0.85,
"snippet": "Linked note...",
"title": null,
"tags": [],
"source": "link_outgoing"
}
],
"context_summary": "These notes are related because they discuss similar API concepts..."
}
The `source` field indicates how each related note was discovered: `"semantic"` (vector similarity), `"link_outgoing"` (direct outgoing graph link), or `"link_incoming"` (incoming graph link). When `context_summary=true` and an inference backend is available, the response includes an LLM-generated explanation of the thematic connection between the notes.
Graph Exploration
Explore Graph
GET /api/v1/graph/{id}?depth=2&max_nodes=50
Traverses semantic links to discover connected notes using recursive CTEs.
Query Parameters:
| Param | Type | Description |
|---|---|---|
| depth | int | Maximum traversal depth (default: 2, max: 10) |
| max_nodes | int | Maximum nodes to return (default: 50, max: 1000) |
| min_score | float | Minimum link score threshold (default: 0.0) |
| max_edges_per_node | int | Maximum edges returned per node (optional) |
| edge_filter | string | Community filter: `all` (default), `intra_community`, `inter_community` |
| include_structural | bool | Include structural collection edges (default: true) |
Response:
{
"nodes": [
{
"id": "550e8400-...",
"title": "Root Note",
"depth": 0
},
{
"id": "660e8400-...",
"title": "Connected Note",
"depth": 1
}
],
"edges": [
{
"from": "550e8400-...",
"to": "660e8400-...",
"score": 0.82
}
]
}
Graph Topology Stats
GET /api/v1/graph/topology/stats
Returns graph topology statistics for the current memory archive.
Response:
{
"total_notes": 1523,
"total_links": 8712,
"isolated_nodes": 42,
"connected_components": 18,
"avg_degree": 11.4,
"max_degree": 87,
"min_degree_linked": 1,
"median_degree": 9.0,
"linking_strategy": "snn_pfnet",
"effective_k": 25
}
Example:
curl http://localhost:3000/api/v1/graph/topology/stats \
-H "Authorization: Bearer <API_KEY>"
Graph Diagnostics
GET /api/v1/graph/diagnostics?sample_size=1000
Returns graph quality diagnostics by sampling embedding pairs.
Query Parameters:
| Param | Type | Description |
|---|---|---|
| sample_size | int | Number of random embedding pairs to sample (default: 1000, range: 10–10000) |
Example:
curl "http://localhost:3000/api/v1/graph/diagnostics?sample_size=500" \
-H "Authorization: Bearer <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:
| Field | Type | Required | Description |
|---|---|---|---|
| label | string | Yes | Human-readable label for the snapshot |
| sample_size | int | No | Embedding pairs to sample (default: 1000) |
List Diagnostics Snapshots
GET /api/v1/graph/diagnostics/history?limit=20
Returns previously captured diagnostics snapshots.
Query Parameters:
| Param | Type | Description |
|---|---|---|
| limit | int | Max snapshots to return (default: 20, max: 100) |
Compare Diagnostics Snapshots
GET /api/v1/graph/diagnostics/compare?before={uuid}&after={uuid}
Compares two diagnostics snapshots and returns a diff.
Query Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
| before | UUID | Yes | Snapshot ID for the "before" state |
| after | UUID | Yes | Snapshot ID for the "after" state |
Recompute SNN Scores
POST /api/v1/graph/snn/recompute
Content-Type: application/json
{
"k": 25,
"threshold": 0.10,
"dry_run": false
}
Recomputes Shared Nearest Neighbor (SNN) scores for all edges.
Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
| k | int | No | Number of nearest neighbors (default: adaptive from graph config) |
| threshold | float | No | SNN threshold — edges below this are pruned (default: 0.10) |
| dry_run | bool | No | Compute scores without updating/deleting anything (default: false) |
PFNET Sparsify
POST /api/v1/graph/pfnet/sparsify
Content-Type: application/json
{
"q": 2,
"dry_run": false
}
Runs Pathfinder Network (PFNET) sparsification to remove redundant edges.
Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
| q | int | No | PFNET q parameter — higher values produce sparser graphs (default: 2, RNG-equivalent) |
| dry_run | bool | No | Compute without deleting/updating edges (default: false) |
Coarse Community Detection
POST /api/v1/graph/community/coarse
Content-Type: application/json
{
"coarse_dim": 64,
"similarity_threshold": 0.3,
"resolution": 1.0
}
Runs Louvain community detection on a dimensionality-reduced (MRL) graph.
Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
| coarse_dim | int | No | MRL truncation dimension (default: 64, range: 2–768) |
| similarity_threshold | float | No | Minimum cosine similarity for edge inclusion (default: 0.3) |
| resolution | float | No | Louvain resolution parameter (default: from config) |
Trigger Graph Maintenance
POST /api/v1/graph/maintenance
Content-Type: application/json
{
"steps": ["normalize", "snn", "pfnet", "snapshot"]
}
Queues a graph maintenance job. Jobs are deduplicated — if one is already pending, returns 200 with `"already_pending"` status.
Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
| steps | string[] | No | Steps to run. Default: all steps. Valid values: `normalize`, `snn`, `pfnet`, `snapshot` |
Response (201 Created — new job):
{
"id": "job-uuid",
"status": "queued",
"steps": ["normalize", "snn", "pfnet", "snapshot"]
}
Response (200 OK — deduplicated):
{
"id": null,
"status": "already_pending"
}
Example:
# Run full maintenance pipeline
curl -X POST http://localhost:3000/api/v1/graph/maintenance \
-H "Authorization: Bearer <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:
| Param | Type | Description |
|---|---|---|
| status | string | Filter by status: `pending`, `processing`, `completed`, `failed` |
| job_type | string | Filter by type: `ai_revision`, `embedding`, etc. |
| limit | int | Max results |
Create Job
POST /api/v1/jobs
Content-Type: application/json
{
"job_type": "ai_revision",
"target_id": "550e8400-...",
"parameters": {
"mode": "full"
}
}
Get Job
GET /api/v1/jobs/{id}
Pending Jobs Count
GET /api/v1/jobs/pending
Returns count of pending jobs.
Queue Stats
GET /api/v1/jobs/stats
Returns queue health metrics:
{
"pending": 5,
"processing": 2,
"completed_last_hour": 150,
"failed_last_hour": 0,
"avg_processing_time_ms": 2341
}
Job Processing Control
Pause and resume job processing globally or per-archive.
Get Pause Status
GET /api/v1/jobs/status
Returns the current pause state and queue statistics:
{
"global": "running",
"archives": {
"research": "paused"
},
"queue": {
"pending": 42,
"running": 3
}
}
Pause All Processing
POST /api/v1/jobs/pause
Pauses job processing globally. Jobs already running will complete, but no new jobs will be picked up.
Resume All Processing
POST /api/v1/jobs/resume
Resumes globally paused job processing.
Pause Archive Processing
POST /api/v1/jobs/pause/{archive}
Pauses job processing for a specific memory archive. Jobs for other archives continue normally.
Resume Archive Processing
POST /api/v1/jobs/resume/{archive}
Resumes job processing for a specific memory archive.
Backup & Export
Fortémi provides multiple backup strategies for different use cases.
JSON Export/Import (Legacy)
Export Backup
GET /api/v1/backup/export
Exports all notes and metadata as JSON.
Download Backup
GET /api/v1/backup/download
Downloads the most recent export as a file.
Import Backup
POST /api/v1/backup/import
Content-Type: multipart/form-data
[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:
| Param | Type | Description |
|---|---|---|
| include | string | Comma-separated `core-v1` components: `notes`, `collections`, `tags`, `templates`, `links` |
| include_blobs | bool | Opt 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:
| Param | Type | Description |
|---|---|---|
| name | string | Memory 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:
| Param | Type | Description |
|---|---|---|
| content | string | `original` or `revised` (default: `revised`) |
| include_frontmatter | bool | Include YAML frontmatter (default: true) |
Memory Management
Fortemi supports parallel memory archives for organizing different knowledge domains with full schema-level isolation.
Request Header
All endpoints support memory routing via the `X-Fortemi-Memory` header:
| Header | Values | Description |
|---|---|---|
| `X-Fortemi-Memory` | Memory name | Routes request to specified memory (default: "default") |
List Memories
GET /api/v1/memories
Returns all memory archives with metadata (name, description, note count, size, schema version).
Create Memory
POST /api/v1/memories
Content-Type: application/json
{
"name": "work-notes",
"description": "Work-related documentation"
}
Response (201 Created):
{
"id": "550e8400-...",
"name": "work-notes",
"schema_name": "archive_work_notes"
}
Returns HTTP 400 if `MAX_MEMORIES` limit is reached.
Get Memory
GET /api/v1/memories/:name
Update Memory
PATCH /api/v1/memories/:name
Content-Type: application/json
{
"description": "Updated description"
}
Delete Memory
DELETE /api/v1/memories/:name
Permanently deletes the memory schema and all data. Cannot be undone.
Set Default Memory
POST /api/v1/archives/:name/default
Sets the specified memory as default. Only one memory can be default at a time.
Get Memory Statistics
GET /api/v1/archives/:name/stats
Returns note count and storage size for a specific memory.
Get Memories Overview
GET /api/v1/memories/overview
Returns aggregate statistics across all memories:
{
"capacity": {
"max_memories": 100,
"current_count": 3,
"available": 97
},
"usage": {
"total_notes": 1768,
"total_size_bytes": 60817408,
"total_size_human": "58.02 MB"
},
"memories": [
{
"name": "default",
"note_count": 1200,
"size_bytes": 41943040,
"is_default": true
}
],
"database": {
"total_size_bytes": 209715200,
"total_size_human": "200.00 MB"
}
}
Clone Memory
POST /api/v1/archives/:name/clone
Content-Type: application/json
{
"new_name": "work-notes-backup",
"description": "Backup before migration"
}
Creates a deep copy of the memory including all notes, embeddings, tags, collections, and relationships.
Response (201 Created):
{
"id": "660e8400-...",
"name": "work-notes-backup",
"schema_name": "archive_work_notes_backup",
"cloned_from": "work-notes"
}
Federated Search
POST /api/v1/search/federated
Content-Type: application/json
{
"query": "project documentation",
"memories": ["default", "work-notes"]
}
Searches across multiple memories in parallel with unified result ranking.
Response:
{
"results": [
{
"note_id": "...",
"memory": "work-notes",
"score": 0.92,
"title": "Project Docs",
"snippet": "...",
"tags": ["project"]
}
],
"total": 1,
"memories_searched": ["default", "work-notes"]
}
Use `"memories": ["all"]` to search every memory.
Vision
Ad-hoc image description using the configured vision LLM. Requires `OLLAMA_VISION_MODEL` to be set.
Describe Image
POST /api/v1/vision/describe
Content-Type: multipart/form-data
Authorization: Bearer <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:
| Field | Type | Required | Description |
|---|---|---|---|
| file | binary | Yes | Image file (JPEG, PNG, WebP, GIF, etc.) |
| prompt | string | No | Custom description prompt |
| model | string | No | Vision model override (e.g., `llava:13b`) |
Response (200 OK):
{
"description": "A sunset photograph showing orange and pink clouds over a city skyline...",
"model": "qwen3.5:9b",
"image_size": 2457600
}
Errors:
- `400 Bad Request`: Missing or empty file
- `503 Service Unavailable`: `OLLAMA_VISION_MODEL` not configured
Example:
curl -X POST http://localhost:3000/api/v1/vision/describe \
-H "Authorization: Bearer <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:
| Field | Type | Required | Description |
|---|---|---|---|
| file | binary | Yes | Audio file (MP3, WAV, M4A, OGG, FLAC, etc.) |
| language | string | No | ISO 639-1 language hint (e.g., `en`, `es`). Auto-detected if omitted. |
| model | string | No | Whisper model override (e.g., `Systran/faster-whisper-large-v3`) |
Response (200 OK):
{
"text": "Hello, this is the full transcription of the audio file...",
"segments": [
{
"start": 0.0,
"end": 3.5,
"text": "Hello, this is the full"
},
{
"start": 3.5,
"end": 7.2,
"text": "transcription of the audio file..."
}
],
"language": "en",
"duration_secs": 7.2,
"model": "Systran/faster-distil-whisper-large-v3",
"audio_size": 115200
}
Errors:
- `400 Bad Request`: Missing or empty file
- `503 Service Unavailable`: `WHISPER_BASE_URL` not configured
Example:
curl -X POST http://localhost:3000/api/v1/audio/transcribe \
-H "Authorization: Bearer <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:
| Field | Type | Required | Description |
|---|---|---|---|
| `input` | string | Yes | The user's message |
| `model` | string | No | Ollama model slug override (e.g., `qwen3.5:9b`). Uses server default if omitted. |
| `context` | object | No | Optional context for the conversation |
| `context.note_id` | string | No | Note ID for context (future RAG integration) |
| `context.collection_id` | string | No | Collection ID for context (future RAG integration) |
| `context.search_query` | string | No | Search query for context (future RAG integration) |
| `context.conversation_history` | array | No | Previous messages for multi-turn conversation |
Each message in `conversation_history`:
| Field | Type | Required | Description |
|---|---|---|---|
| `role` | string | Yes | `user` or `assistant` |
| `content` | string | Yes | Message content |
| `timestamp` | string | No | ISO 8601 timestamp |
Response (200 OK):
{
"messages": [
{
"role": "assistant",
"content": "Based on your recent notes, the key themes include..."
}
],
"actions": [],
"model_info": {
"model": "qwen3.5:9b",
"context_window": 32768,
"estimated_available_context": 32568,
"max_output_tokens": 8192,
"supports_thinking": true,
"thinking_type": "explicit_tags",
"speed_tok_s": 45.0,
"parameter_size": "9B",
"family": "qwen3.5"
}
}
Model Info Fields:
| Field | Type | Description |
|---|---|---|
| `model` | string | Model slug used for generation |
| `context_window` | integer | Total context window in tokens |
| `estimated_available_context` | integer | Context remaining after system prompt overhead |
| `max_output_tokens` | integer | Maximum output tokens per response |
| `supports_thinking` | boolean | Whether the model supports chain-of-thought reasoning |
| `thinking_type` | string | One of: `explicit_tags`, `verbose_reasoning`, `pattern_based`, `none`, `not_tested` |
| `speed_tok_s` | float | Estimated generation speed in tokens/second |
| `parameter_size` | string | Model parameter count (e.g., `8B`, `70B`) |
| `family` | string | Model family (e.g., `qwen3`, `llama3`) |
Errors:
- `400 Bad Request`: Empty input
- `503 Service Unavailable`: Chat not configured (Ollama unreachable) or all GPU threads busy
When busy, the 503 response 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.
| Endpoint | Method | Description |
|---|---|---|
| `/api/v1/pke/keysets` | GET | List all keysets |
| `/api/v1/pke/keysets` | POST | Create a new keyset |
| `/api/v1/pke/keysets/active` | GET | Get the currently active keyset |
| `/api/v1/pke/keysets/import` | POST | Import a keyset from JSON |
| `/api/v1/pke/keysets/{name_or_id}` | DELETE | Delete a keyset |
| `/api/v1/pke/keysets/{name_or_id}/active` | PUT | Set a keyset as active |
| `/api/v1/pke/keysets/{name_or_id}/export` | GET | Export a keyset as JSON |
Real-Time Events
Fortémi provides real-time event streaming through three channels. For comprehensive documentation, see Real-Time Events.
SSE (Server-Sent Events)
GET /api/v1/events
Streams all server events as `text/event-stream`. Each event includes an `event:` type field and `data:` JSON payload. Keep-alive sent every 15 seconds.
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.
| 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": "<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:
| Endpoint | Method | Hosted behavior |
|---|---|---|
| `/api/v1/user/secrets` | POST | Store a provider credential through the configured KMS provider. |
| `/api/v1/user/secrets` | GET | Return metadata only for the authenticated user. |
| `/api/v1/user/secrets/{id}` | DELETE | Idempotently revoke an opaque credential row. |
| `/api/v1/inference/embed` | POST | Embed with a stored credential and operator-approved provider/model profile. |
| `/api/v1/inference/catalog` | GET | Return 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:
| Status | Type URI | Description |
|---|---|---|
| 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
- 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