# Internal API Reference ## Table of Contents 1. [Introduction](#introduction) 2. [Authentication and Session Management](#authentication-and-session-management) 3. [Core API Endpoints](#core-api-endpoints) 4. [WebSocket Real-time Updates](#websocket-real-time-updates) 5. [File Operations](#file-operations) 6. [Import/Export Operations](#import-export-operations) 7. [Synchronization API](#synchronization-api) 8. [When to Use Internal vs ETAPI](#when-to-use-internal-vs-etapi) 9. [Security Considerations](#security-considerations) ## Introduction The Internal API is the primary interface used by the Trilium Notes client application to communicate with the server. While powerful and feature-complete, this API is primarily designed for internal use. ### Important Notice **For external integrations, please use [ETAPI](./ETAPI%20Complete%20Guide.md) instead.** The Internal API: - May change between versions without notice - Requires session-based authentication with CSRF protection - Is tightly coupled with the frontend application - Has limited documentation and stability guarantees ### Base URL ``` http://localhost:8080/api ``` ### Key Characteristics - Session-based authentication with cookies - CSRF token protection for state-changing operations - WebSocket support for real-time updates - Full feature parity with the Trilium UI - Complex request/response formats optimized for the client ## Authentication and Session Management ### Password Login **POST** `/api/login` Authenticates user with password and creates a session. **Request:** ```javascript const formData = new URLSearchParams(); formData.append('password', 'your-password'); const response = await fetch('http://localhost:8080/api/login', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formData, credentials: 'include' // Important for cookie handling }); ``` **Response:** ```json { "success": true, "message": "Login successful" } ``` The server sets a session cookie (`trilium.sid`) that must be included in subsequent requests. ### TOTP Authentication (2FA) If 2FA is enabled, include the TOTP token: ```javascript formData.append('password', 'your-password'); formData.append('totpToken', '123456'); ``` ### Token Authentication **POST** `/api/login/token` Generate an API token for programmatic access: ```javascript const response = await fetch('http://localhost:8080/api/login/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: 'your-password', tokenName: 'My Integration' }) }); const { authToken } = await response.json(); // Use this token in Authorization header for future requests ``` ### Protected Session **POST** `/api/login/protected` Enter protected session to access encrypted notes: ```javascript await fetch('http://localhost:8080/api/login/protected', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ password: 'your-password' }), credentials: 'include' }); ``` ### Logout **POST** `/api/logout` ```javascript await fetch('http://localhost:8080/api/logout', { method: 'POST', headers: { 'X-CSRF-Token': csrfToken }, credentials: 'include' }); ``` ## Core API Endpoints ### Notes #### Get Note **GET** `/api/notes/{noteId}` ```javascript const response = await fetch('http://localhost:8080/api/notes/root', { credentials: 'include' }); const note = await response.json(); ``` **Response:** ```json { "noteId": "root", "title": "Trilium Notes", "type": "text", "mime": "text/html", "isProtected": false, "isDeleted": false, "dateCreated": "2024-01-01 00:00:00.000+0000", "dateModified": "2024-01-15 10:30:00.000+0000", "utcDateCreated": "2024-01-01 00:00:00.000Z", "utcDateModified": "2024-01-15 10:30:00.000Z", "parentBranches": [ { "branchId": "root_root", "parentNoteId": "none", "prefix": null, "notePosition": 10 } ], "attributes": [], "cssClass": "", "iconClass": "bx bx-folder" } ``` #### Create Note **POST** `/api/notes/{parentNoteId}/children` ```javascript const response = await fetch('http://localhost:8080/api/notes/root/children', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ title: 'New Note', type: 'text', content: '
Note content
', isProtected: false }), credentials: 'include' }); const { note, branch } = await response.json(); ``` #### Update Note **PUT** `/api/notes/{noteId}` ```javascript await fetch(`http://localhost:8080/api/notes/${noteId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ title: 'Updated Title', type: 'text', mime: 'text/html' }), credentials: 'include' }); ``` #### Delete Note **DELETE** `/api/notes/{noteId}` ```javascript await fetch(`http://localhost:8080/api/notes/${noteId}`, { method: 'DELETE', headers: { 'X-CSRF-Token': csrfToken }, credentials: 'include' }); ``` #### Get Note Content **GET** `/api/notes/{noteId}/content` Returns the actual content of the note: ```javascript const response = await fetch(`http://localhost:8080/api/notes/${noteId}/content`, { credentials: 'include' }); const content = await response.text(); ``` #### Save Note Content **PUT** `/api/notes/{noteId}/content` ```javascript await fetch(`http://localhost:8080/api/notes/${noteId}/content`, { method: 'PUT', headers: { 'Content-Type': 'text/html', 'X-CSRF-Token': csrfToken }, body: 'Updated content
', credentials: 'include' }); ``` ### Tree Operations #### Get Branch **GET** `/api/branches/{branchId}` ```javascript const branch = await fetch(`http://localhost:8080/api/branches/${branchId}`, { credentials: 'include' }).then(r => r.json()); ``` #### Move Note **PUT** `/api/branches/{branchId}/move` ```javascript await fetch(`http://localhost:8080/api/branches/${branchId}/move`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ parentNoteId: 'newParentId', beforeNoteId: 'siblingNoteId' // optional, for positioning }), credentials: 'include' }); ``` #### Clone Note **POST** `/api/notes/{noteId}/clone` ```javascript const response = await fetch(`http://localhost:8080/api/notes/${noteId}/clone`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ parentNoteId: 'targetParentId', prefix: 'Copy of ' }), credentials: 'include' }); ``` #### Sort Child Notes **PUT** `/api/notes/{noteId}/sort-children` ```javascript await fetch(`http://localhost:8080/api/notes/${noteId}/sort-children`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ sortBy: 'title', // or 'dateCreated', 'dateModified' reverse: false }), credentials: 'include' }); ``` ### Attributes #### Create Attribute **POST** `/api/notes/{noteId}/attributes` ```javascript const response = await fetch(`http://localhost:8080/api/notes/${noteId}/attributes`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ type: 'label', name: 'todo', value: '', isInheritable: false }), credentials: 'include' }); ``` #### Update Attribute **PUT** `/api/attributes/{attributeId}` ```javascript await fetch(`http://localhost:8080/api/attributes/${attributeId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ value: 'updated value' }), credentials: 'include' }); ``` #### Delete Attribute **DELETE** `/api/attributes/{attributeId}` ```javascript await fetch(`http://localhost:8080/api/attributes/${attributeId}`, { method: 'DELETE', headers: { 'X-CSRF-Token': csrfToken }, credentials: 'include' }); ``` ### Search #### Search Notes **GET** `/api/search` ```javascript const params = new URLSearchParams({ query: '#todo OR #task', fastSearch: 'false', includeArchivedNotes: 'false', ancestorNoteId: 'root', orderBy: 'relevancy', orderDirection: 'desc', limit: '50' }); const response = await fetch(`http://localhost:8080/api/search?${params}`, { credentials: 'include' }); const { results } = await response.json(); ``` #### Search Note Map **GET** `/api/search-note-map` Returns hierarchical structure of search results: ```javascript const params = new URLSearchParams({ query: 'project', maxDepth: '3' }); const noteMap = await fetch(`http://localhost:8080/api/search-note-map?${params}`, { credentials: 'include' }).then(r => r.json()); ``` ### Revisions #### Get Note Revisions **GET** `/api/notes/{noteId}/revisions` ```javascript const revisions = await fetch(`http://localhost:8080/api/notes/${noteId}/revisions`, { credentials: 'include' }).then(r => r.json()); ``` #### Get Revision Content **GET** `/api/revisions/{revisionId}/content` ```javascript const content = await fetch(`http://localhost:8080/api/revisions/${revisionId}/content`, { credentials: 'include' }).then(r => r.text()); ``` #### Restore Revision **POST** `/api/revisions/{revisionId}/restore` ```javascript await fetch(`http://localhost:8080/api/revisions/${revisionId}/restore`, { method: 'POST', headers: { 'X-CSRF-Token': csrfToken }, credentials: 'include' }); ``` #### Delete Revision **DELETE** `/api/revisions/{revisionId}` ```javascript await fetch(`http://localhost:8080/api/revisions/${revisionId}`, { method: 'DELETE', headers: { 'X-CSRF-Token': csrfToken }, credentials: 'include' }); ``` ## WebSocket Real-time Updates The Internal API provides WebSocket connections for real-time synchronization and updates. ### Connection Setup ```javascript class TriliumWebSocket { constructor() { this.ws = null; this.reconnectInterval = 5000; this.shouldReconnect = true; } connect() { // WebSocket URL same as base URL but with ws:// protocol const wsUrl = 'ws://localhost:8080'; this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { console.log('WebSocket connected'); this.sendPing(); }; this.ws.onmessage = (event) => { const message = JSON.parse(event.data); this.handleMessage(message); }; this.ws.onerror = (error) => { console.error('WebSocket error:', error); }; this.ws.onclose = () => { console.log('WebSocket disconnected'); if (this.shouldReconnect) { setTimeout(() => this.connect(), this.reconnectInterval); } }; } handleMessage(message) { switch (message.type) { case 'sync': this.handleSync(message.data); break; case 'entity-changes': this.handleEntityChanges(message.data); break; case 'refresh-tree': this.refreshTree(); break; case 'create-note': this.handleNoteCreated(message.data); break; case 'update-note': this.handleNoteUpdated(message.data); break; case 'delete-note': this.handleNoteDeleted(message.data); break; default: console.log('Unknown message type:', message.type); } } sendPing() { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: 'ping' })); setTimeout(() => this.sendPing(), 30000); // Ping every 30 seconds } } send(type, data) { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type, data })); } } handleSync(data) { // Handle synchronization data console.log('Sync data received:', data); } handleEntityChanges(changes) { // Handle entity change notifications changes.forEach(change => { console.log(`Entity ${change.entityName} ${change.entityId} changed`); }); } refreshTree() { // Refresh the note tree UI console.log('Tree refresh requested'); } handleNoteCreated(note) { console.log('Note created:', note); } handleNoteUpdated(note) { console.log('Note updated:', note); } handleNoteDeleted(noteId) { console.log('Note deleted:', noteId); } disconnect() { this.shouldReconnect = false; if (this.ws) { this.ws.close(); } } } // Usage const ws = new TriliumWebSocket(); ws.connect(); // Send custom message ws.send('log-info', { info: 'Client started' }); // Clean up on page unload window.addEventListener('beforeunload', () => { ws.disconnect(); }); ``` ### Message Types #### Incoming Messages | Type | Description | Data Format | |------|-------------|-------------| | `sync` | Synchronization data | `{ entityChanges: [], lastSyncedPush: number }` | | `entity-changes` | Entity modifications | `[{ entityName, entityId, action }]` | | `refresh-tree` | Tree structure changed | None | | `create-note` | Note created | Note object | | `update-note` | Note updated | Note object | | `delete-note` | Note deleted | `{ noteId }` | | `frontend-script` | Execute frontend script | `{ script, params }` | #### Outgoing Messages | Type | Description | Data Format | |------|-------------|-------------| | `ping` | Keep connection alive | None | | `log-error` | Log client error | `{ error, stack }` | | `log-info` | Log client info | `{ info }` | ### Real-time Collaboration Example ```javascript class CollaborativeEditor { constructor(noteId) { this.noteId = noteId; this.ws = new TriliumWebSocket(); this.content = ''; this.lastSaved = ''; this.ws.handleNoteUpdated = (note) => { if (note.noteId === this.noteId) { this.handleRemoteUpdate(note); } }; } async loadNote() { const response = await fetch(`/api/notes/${this.noteId}/content`, { credentials: 'include' }); this.content = await response.text(); this.lastSaved = this.content; } handleRemoteUpdate(note) { // Check if the update is from another client if (this.content !== this.lastSaved) { // Show conflict resolution UI this.showConflictDialog(note); } else { // Apply remote changes this.loadNote(); } } async saveContent(content) { this.content = content; await fetch(`/api/notes/${this.noteId}/content`, { method: 'PUT', headers: { 'Content-Type': 'text/html', 'X-CSRF-Token': csrfToken }, body: content, credentials: 'include' }); this.lastSaved = content; } showConflictDialog(remoteNote) { // Implementation of conflict resolution UI console.log('Conflict detected with remote changes'); } } ``` ## File Operations ### Upload File **POST** `/api/notes/{noteId}/attachments/upload` ```javascript const formData = new FormData(); formData.append('file', fileInput.files[0]); const response = await fetch(`/api/notes/${noteId}/attachments/upload`, { method: 'POST', headers: { 'X-CSRF-Token': csrfToken }, body: formData, credentials: 'include' }); const attachment = await response.json(); ``` ### Download Attachment **GET** `/api/attachments/{attachmentId}/download` ```javascript const response = await fetch(`/api/attachments/${attachmentId}/download`, { credentials: 'include' }); const blob = await response.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'attachment.pdf'; a.click(); ``` ### Upload Image **POST** `/api/images/upload` ```javascript const formData = new FormData(); formData.append('image', imageFile); formData.append('noteId', noteId); const response = await fetch('/api/images/upload', { method: 'POST', headers: { 'X-CSRF-Token': csrfToken }, body: formData, credentials: 'include' }); const { url, noteId: imageNoteId } = await response.json(); ``` ## Import/Export Operations ### Import ZIP **POST** `/api/import` ```javascript const formData = new FormData(); formData.append('file', zipFile); formData.append('parentNoteId', 'root'); const response = await fetch('/api/import', { method: 'POST', headers: { 'X-CSRF-Token': csrfToken }, body: formData, credentials: 'include' }); const result = await response.json(); ``` ### Export Subtree **GET** `/api/notes/{noteId}/export` ```javascript const params = new URLSearchParams({ format: 'html', // or 'markdown' exportRevisions: 'true' }); const response = await fetch(`/api/notes/${noteId}/export?${params}`, { credentials: 'include' }); const blob = await response.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'export.zip'; a.click(); ``` ### Import Markdown **POST** `/api/import/markdown` ```javascript const response = await fetch('/api/import/markdown', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ parentNoteId: 'root', content: '# Markdown Content\n\nParagraph text...', title: 'Imported from Markdown' }), credentials: 'include' }); ``` ### Export as PDF **GET** `/api/notes/{noteId}/export/pdf` ```javascript const response = await fetch(`/api/notes/${noteId}/export/pdf`, { credentials: 'include' }); const blob = await response.blob(); const url = URL.createObjectURL(blob); window.open(url, '_blank'); ``` ## Synchronization API ### Get Sync Status **GET** `/api/sync/status` ```javascript const status = await fetch('/api/sync/status', { credentials: 'include' }).then(r => r.json()); console.log('Sync enabled:', status.syncEnabled); console.log('Last sync:', status.lastSyncedPush); ``` ### Force Sync **POST** `/api/sync/now` ```javascript await fetch('/api/sync/now', { method: 'POST', headers: { 'X-CSRF-Token': csrfToken }, credentials: 'include' }); ``` ### Get Sync Log **GET** `/api/sync/log` ```javascript const log = await fetch('/api/sync/log', { credentials: 'include' }).then(r => r.json()); log.forEach(entry => { console.log(`${entry.date}: ${entry.message}`); }); ``` ## Script Execution ### Execute Script **POST** `/api/script/run` ```javascript const response = await fetch('/api/script/run', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ script: ` const note = await api.getNote('root'); return { title: note.title, children: note.children.length }; `, params: {} }), credentials: 'include' }); const result = await response.json(); ``` ### Execute Note Script **POST** `/api/notes/{noteId}/run` Run a script note: ```javascript const response = await fetch(`/api/notes/${scriptNoteId}/run`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ params: { targetNoteId: 'someNoteId' } }), credentials: 'include' }); const result = await response.json(); ``` ## Special Features ### Calendar API #### Get Day Note **GET** `/api/calendar/days/{date}` ```javascript const date = '2024-01-15'; const dayNote = await fetch(`/api/calendar/days/${date}`, { credentials: 'include' }).then(r => r.json()); ``` #### Get Week Note **GET** `/api/calendar/weeks/{date}` ```javascript const weekNote = await fetch(`/api/calendar/weeks/2024-01-15`, { credentials: 'include' }).then(r => r.json()); ``` #### Get Month Note **GET** `/api/calendar/months/{month}` ```javascript const monthNote = await fetch(`/api/calendar/months/2024-01`, { credentials: 'include' }).then(r => r.json()); ``` ### Inbox Note **GET** `/api/inbox/{date}` ```javascript const inboxNote = await fetch(`/api/inbox/2024-01-15`, { credentials: 'include' }).then(r => r.json()); ``` ### Note Map **GET** `/api/notes/{noteId}/map` Get visual map data for a note: ```javascript const mapData = await fetch(`/api/notes/${noteId}/map`, { credentials: 'include' }).then(r => r.json()); // Returns nodes and links for visualization console.log('Nodes:', mapData.nodes); console.log('Links:', mapData.links); ``` ### Similar Notes **GET** `/api/notes/{noteId}/similar` Find notes similar to the given note: ```javascript const similarNotes = await fetch(`/api/notes/${noteId}/similar`, { credentials: 'include' }).then(r => r.json()); ``` ## Options and Configuration ### Get All Options **GET** `/api/options` ```javascript const options = await fetch('/api/options', { credentials: 'include' }).then(r => r.json()); ``` ### Update Option **PUT** `/api/options/{optionName}` ```javascript await fetch(`/api/options/theme`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ value: 'dark' }), credentials: 'include' }); ``` ### Get User Preferences **GET** `/api/options/user` ```javascript const preferences = await fetch('/api/options/user', { credentials: 'include' }).then(r => r.json()); ``` ## Database Operations ### Backup Database **POST** `/api/database/backup` ```javascript const response = await fetch('/api/database/backup', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ backupName: 'manual-backup' }), credentials: 'include' }); const { backupFile } = await response.json(); ``` ### Vacuum Database **POST** `/api/database/vacuum` ```javascript await fetch('/api/database/vacuum', { method: 'POST', headers: { 'X-CSRF-Token': csrfToken }, credentials: 'include' }); ``` ### Get Database Info **GET** `/api/database/info` ```javascript const info = await fetch('/api/database/info', { credentials: 'include' }).then(r => r.json()); console.log('Database size:', info.size); console.log('Note count:', info.noteCount); console.log('Revision count:', info.revisionCount); ``` ## When to Use Internal vs ETAPI ### Use Internal API When: - Building custom Trilium clients - Needing WebSocket real-time updates - Requiring full feature parity with the UI - Working within the Trilium frontend environment - Accessing advanced features not available in ETAPI ### Use ETAPI When: - Building external integrations - Creating automation scripts - Developing third-party applications - Needing stable, documented API - Working with different programming languages ### Feature Comparison | Feature | Internal API | ETAPI | |---------|-------------|--------| | **Authentication** | Session/Cookie | Token | | **CSRF Protection** | Required | Not needed | | **WebSocket** | Yes | No | | **Stability** | May change | Stable | | **Documentation** | Limited | Comprehensive | | **Real-time updates** | Yes | No | | **File uploads** | Complex | Simple | | **Scripting** | Full support | Limited | | **Synchronization** | Yes | No | ## Security Considerations ### CSRF Protection All state-changing operations require a CSRF token: ```javascript // Get CSRF token from meta tag or API async function getCsrfToken() { const response = await fetch('/api/csrf-token', { credentials: 'include' }); const { token } = await response.json(); return token; } // Use in requests const csrfToken = await getCsrfToken(); await fetch('/api/notes', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify(data), credentials: 'include' }); ``` ### Session Management ```javascript class TriliumSession { constructor() { this.isAuthenticated = false; this.csrfToken = null; } async login(password) { const formData = new URLSearchParams(); formData.append('password', password); const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formData, credentials: 'include' }); if (response.ok) { this.isAuthenticated = true; this.csrfToken = await this.getCsrfToken(); return true; } return false; } async getCsrfToken() { const response = await fetch('/api/csrf-token', { credentials: 'include' }); const { token } = await response.json(); return token; } async request(url, options = {}) { if (!this.isAuthenticated) { throw new Error('Not authenticated'); } const headers = { ...options.headers }; if (options.method && options.method !== 'GET') { headers['X-CSRF-Token'] = this.csrfToken; } return fetch(url, { ...options, headers, credentials: 'include' }); } async logout() { await this.request('/api/logout', { method: 'POST' }); this.isAuthenticated = false; this.csrfToken = null; } } // Usage const session = new TriliumSession(); await session.login('password'); // Make authenticated requests const notes = await session.request('/api/notes/root').then(r => r.json()); // Create note with CSRF protection await session.request('/api/notes/root/children', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: 'New Note', type: 'text' }) }); await session.logout(); ``` ### Protected Notes Handle encrypted notes properly: ```javascript class ProtectedNoteHandler { constructor(session) { this.session = session; this.protectedSessionTimeout = null; } async enterProtectedSession(password) { const response = await this.session.request('/api/login/protected', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password }) }); if (response.ok) { // Protected session expires after inactivity this.resetProtectedSessionTimeout(); return true; } return false; } resetProtectedSessionTimeout() { if (this.protectedSessionTimeout) { clearTimeout(this.protectedSessionTimeout); } // Assume 5 minute timeout this.protectedSessionTimeout = setTimeout(() => { console.log('Protected session expired'); this.onProtectedSessionExpired(); }, 5 * 60 * 1000); } async accessProtectedNote(noteId) { try { const note = await this.session.request(`/api/notes/${noteId}`) .then(r => r.json()); if (note.isProtected) { // Reset timeout on successful access this.resetProtectedSessionTimeout(); } return note; } catch (error) { if (error.message.includes('Protected session required')) { // Prompt for password const password = await this.promptForPassword(); if (await this.enterProtectedSession(password)) { return this.accessProtectedNote(noteId); } } throw error; } } async promptForPassword() { // Implementation depends on UI framework return prompt('Enter password for protected notes:'); } onProtectedSessionExpired() { // Handle expiration (e.g., show notification, lock UI) console.log('Please re-enter password to access protected notes'); } } ``` ## Error Handling ### Common Error Responses ```javascript // 401 Unauthorized { "status": 401, "message": "Authentication required" } // 403 Forbidden { "status": 403, "message": "CSRF token validation failed" } // 404 Not Found { "status": 404, "message": "Note 'invalidId' not found" } // 400 Bad Request { "status": 400, "message": "Invalid note type: 'invalid'" } // 500 Internal Server Error { "status": 500, "message": "Database error", "stack": "..." // Only in development } ``` ### Error Handler Implementation ```javascript class APIErrorHandler { async handleResponse(response) { if (!response.ok) { const error = await this.parseError(response); switch (response.status) { case 401: this.handleAuthError(error); break; case 403: this.handleForbiddenError(error); break; case 404: this.handleNotFoundError(error); break; case 400: this.handleBadRequestError(error); break; case 500: this.handleServerError(error); break; default: this.handleGenericError(error); } throw error; } return response; } async parseError(response) { try { const errorData = await response.json(); return new APIError( response.status, errorData.message || response.statusText, errorData ); } catch { return new APIError( response.status, response.statusText ); } } handleAuthError(error) { console.error('Authentication required'); // Redirect to login window.location.href = '/login'; } handleForbiddenError(error) { if (error.message.includes('CSRF')) { console.error('CSRF token invalid, refreshing...'); // Refresh CSRF token this.refreshCsrfToken(); } else { console.error('Access forbidden:', error.message); } } handleNotFoundError(error) { console.error('Resource not found:', error.message); } handleBadRequestError(error) { console.error('Bad request:', error.message); } handleServerError(error) { console.error('Server error:', error.message); // Show user-friendly error message this.showErrorNotification('An error occurred. Please try again later.'); } handleGenericError(error) { console.error('API error:', error); } showErrorNotification(message) { // Implementation depends on UI framework alert(message); } } class APIError extends Error { constructor(status, message, data = {}) { super(message); this.status = status; this.data = data; this.name = 'APIError'; } } ``` ## Performance Optimization ### Request Batching ```javascript class BatchedAPIClient { constructor() { this.batchQueue = []; this.batchTimeout = null; this.batchDelay = 50; // ms } async batchRequest(request) { return new Promise((resolve, reject) => { this.batchQueue.push({ request, resolve, reject }); if (!this.batchTimeout) { this.batchTimeout = setTimeout(() => { this.processBatch(); }, this.batchDelay); } }); } async processBatch() { const batch = this.batchQueue.splice(0); this.batchTimeout = null; if (batch.length === 0) return; try { const response = await fetch('/api/batch', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ requests: batch.map(b => b.request) }), credentials: 'include' }); const results = await response.json(); batch.forEach((item, index) => { if (results[index].error) { item.reject(new Error(results[index].error)); } else { item.resolve(results[index].data); } }); } catch (error) { batch.forEach(item => item.reject(error)); } } async getNote(noteId) { return this.batchRequest({ method: 'GET', url: `/api/notes/${noteId}` }); } async getAttribute(attributeId) { return this.batchRequest({ method: 'GET', url: `/api/attributes/${attributeId}` }); } } // Usage const client = new BatchedAPIClient(); // These requests will be batched const [note1, note2, note3] = await Promise.all([ client.getNote('noteId1'), client.getNote('noteId2'), client.getNote('noteId3') ]); ``` ### Caching Strategy ```javascript class CachedAPIClient { constructor() { this.cache = new Map(); this.cacheExpiry = new Map(); this.defaultTTL = 5 * 60 * 1000; // 5 minutes } getCacheKey(method, url, params = {}) { return `${method}:${url}:${JSON.stringify(params)}`; } isExpired(key) { const expiry = this.cacheExpiry.get(key); return !expiry || Date.now() > expiry; } async cachedRequest(method, url, options = {}, ttl = this.defaultTTL) { const key = this.getCacheKey(method, url, options.params); if (method === 'GET' && this.cache.has(key) && !this.isExpired(key)) { return this.cache.get(key); } const response = await fetch(url, { method, ...options, credentials: 'include' }); const data = await response.json(); if (method === 'GET') { this.cache.set(key, data); this.cacheExpiry.set(key, Date.now() + ttl); } return data; } invalidate(pattern) { for (const key of this.cache.keys()) { if (key.includes(pattern)) { this.cache.delete(key); this.cacheExpiry.delete(key); } } } async getNote(noteId) { return this.cachedRequest('GET', `/api/notes/${noteId}`); } async updateNote(noteId, data) { const result = await fetch(`/api/notes/${noteId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify(data), credentials: 'include' }).then(r => r.json()); // Invalidate cache for this note this.invalidate(`/api/notes/${noteId}`); return result; } } ``` ## Advanced Examples ### Building a Note Explorer ```javascript class NoteExplorer { constructor() { this.currentNote = null; this.history = []; this.historyIndex = -1; } async navigateToNote(noteId) { // Add to history if (this.historyIndex < this.history.length - 1) { this.history = this.history.slice(0, this.historyIndex + 1); } this.history.push(noteId); this.historyIndex++; // Load note this.currentNote = await this.loadNoteWithChildren(noteId); this.render(); } async loadNoteWithChildren(noteId) { const [note, children] = await Promise.all([ fetch(`/api/notes/${noteId}`, { credentials: 'include' }) .then(r => r.json()), fetch(`/api/notes/${noteId}/children`, { credentials: 'include' }) .then(r => r.json()) ]); return { ...note, children }; } canGoBack() { return this.historyIndex > 0; } canGoForward() { return this.historyIndex < this.history.length - 1; } async goBack() { if (this.canGoBack()) { this.historyIndex--; const noteId = this.history[this.historyIndex]; this.currentNote = await this.loadNoteWithChildren(noteId); this.render(); } } async goForward() { if (this.canGoForward()) { this.historyIndex++; const noteId = this.history[this.historyIndex]; this.currentNote = await this.loadNoteWithChildren(noteId); this.render(); } } async searchInSubtree(query) { const params = new URLSearchParams({ query: query, ancestorNoteId: this.currentNote.noteId, includeArchivedNotes: 'false' }); const response = await fetch(`/api/search?${params}`, { credentials: 'include' }); return response.json(); } async createChildNote(title, content, type = 'text') { const response = await fetch(`/api/notes/${this.currentNote.noteId}/children`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': await getCsrfToken() }, body: JSON.stringify({ title, content, type }), credentials: 'include' }); const result = await response.json(); // Refresh current note to show new child this.currentNote = await this.loadNoteWithChildren(this.currentNote.noteId); this.render(); return result; } render() { // Render UI - implementation depends on framework console.log('Current note:', this.currentNote.title); console.log('Children:', this.currentNote.children.map(c => c.title)); } } // Usage const explorer = new NoteExplorer(); await explorer.navigateToNote('root'); await explorer.createChildNote('New Child', 'Content
'); const searchResults = await explorer.searchInSubtree('keyword'); ``` ### Building a Task Management System ```javascript class TaskManager { constructor() { this.taskRootId = null; this.csrfToken = null; } async initialize() { this.csrfToken = await getCsrfToken(); this.taskRootId = await this.getOrCreateTaskRoot(); } async getOrCreateTaskRoot() { // Search for existing task root const searchParams = new URLSearchParams({ query: '#taskRoot' }); const searchResponse = await fetch(`/api/search?${searchParams}`, { credentials: 'include' }); const { results } = await searchResponse.json(); if (results.length > 0) { return results[0].noteId; } // Create task root const response = await fetch('/api/notes/root/children', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.csrfToken }, body: JSON.stringify({ title: 'Tasks', type: 'text', content: '${description}
` }), credentials: 'include' }); const { note } = await response.json(); // Add task metadata await Promise.all([ this.addLabel(note.noteId, 'task'), this.addLabel(note.noteId, 'status', 'todo'), this.addLabel(note.noteId, 'priority', priority), dueDate ? this.addLabel(note.noteId, 'dueDate', dueDate) : null ].filter(Boolean)); return note; } async addLabel(noteId, name, value = '') { await fetch(`/api/notes/${noteId}/attributes`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.csrfToken }, body: JSON.stringify({ type: 'label', name, value, isInheritable: false }), credentials: 'include' }); } async getTasks(status = null, priority = null) { let query = '#task'; if (status) query += ` #status=${status}`; if (priority) query += ` #priority=${priority}`; const params = new URLSearchParams({ query, ancestorNoteId: this.taskRootId, orderBy: 'dateModified', orderDirection: 'desc' }); const response = await fetch(`/api/search?${params}`, { credentials: 'include' }); const { results } = await response.json(); return results; } async updateTaskStatus(noteId, newStatus) { // Get task attributes const note = await fetch(`/api/notes/${noteId}`, { credentials: 'include' }).then(r => r.json()); // Find status attribute const statusAttr = note.attributes.find(a => a.name === 'status'); if (statusAttr) { // Update existing status await fetch(`/api/attributes/${statusAttr.attributeId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.csrfToken }, body: JSON.stringify({ value: newStatus }), credentials: 'include' }); } else { // Add status attribute await this.addLabel(noteId, 'status', newStatus); } // Add completion timestamp if marking as done if (newStatus === 'done') { const timestamp = new Date().toISOString(); await this.addLabel(noteId, 'completedAt', timestamp); } } async getTaskStats() { const [todoTasks, inProgressTasks, doneTasks] = await Promise.all([ this.getTasks('todo'), this.getTasks('in-progress'), this.getTasks('done') ]); return { todo: todoTasks.length, inProgress: inProgressTasks.length, done: doneTasks.length, total: todoTasks.length + inProgressTasks.length + doneTasks.length }; } } // Usage const taskManager = new TaskManager(); await taskManager.initialize(); // Create tasks const task1 = await taskManager.createTask( 'Review Documentation', 'Review and update API documentation', 'high', '2024-01-20' ); const task2 = await taskManager.createTask( 'Fix Bug #123', 'Investigate and fix the reported issue', 'medium' ); // Get tasks const todoTasks = await taskManager.getTasks('todo'); console.log('Todo tasks:', todoTasks); // Update task status await taskManager.updateTaskStatus(task1.noteId, 'in-progress'); // Get statistics const stats = await taskManager.getTaskStats(); console.log('Task statistics:', stats); ``` ## Conclusion The Internal API provides complete access to Trilium's functionality but should be used with caution due to its complexity and potential for changes. For most external integrations, [ETAPI](./ETAPI%20Complete%20Guide.md) is the recommended choice due to its stability and comprehensive documentation. Key takeaways: - Always include CSRF tokens for state-changing operations - Handle session management carefully - Use WebSocket for real-time updates - Implement proper error handling - Consider using ETAPI for external integrations - Cache responses when appropriate for better performance For additional information, refer to: - [ETAPI Complete Guide](./ETAPI%20Complete%20Guide.md) - [Script API Cookbook](./Script%20API%20Cookbook.md) - [WebSocket API Documentation](./WebSocket%20API.md)