Files
Trilium/docs/Developer Guide/API Documentation/Internal API Reference.md
2025-08-21 15:55:44 +00:00

46 KiB
Vendored

Internal API Reference

Table of Contents

  1. Introduction
  2. Authentication and Session Management
  3. Core API Endpoints
  4. WebSocket Real-time Updates
  5. File Operations
  6. Import/Export Operations
  7. Synchronization API
  8. When to Use Internal vs ETAPI
  9. 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 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:

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:

{
    "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:

formData.append('password', 'your-password');
formData.append('totpToken', '123456');

Token Authentication

POST /api/login/token

Generate an API token for programmatic access:

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:

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

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}

const response = await fetch('http://localhost:8080/api/notes/root', {
    credentials: 'include'
});

const note = await response.json();

Response:

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

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: '<p>Note content</p>',
        isProtected: false
    }),
    credentials: 'include'
});

const { note, branch } = await response.json();

Update Note

PUT /api/notes/{noteId}

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}

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:

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

await fetch(`http://localhost:8080/api/notes/${noteId}/content`, {
    method: 'PUT',
    headers: {
        'Content-Type': 'text/html',
        'X-CSRF-Token': csrfToken
    },
    body: '<p>Updated content</p>',
    credentials: 'include'
});

Tree Operations

Get Branch

GET /api/branches/{branchId}

const branch = await fetch(`http://localhost:8080/api/branches/${branchId}`, {
    credentials: 'include'
}).then(r => r.json());

Move Note

PUT /api/branches/{branchId}/move

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

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

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

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}

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}

await fetch(`http://localhost:8080/api/attributes/${attributeId}`, {
    method: 'DELETE',
    headers: {
        'X-CSRF-Token': csrfToken
    },
    credentials: 'include'
});

Search Notes

GET /api/search

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:

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

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

const content = await fetch(`http://localhost:8080/api/revisions/${revisionId}/content`, {
    credentials: 'include'
}).then(r => r.text());

Restore Revision

POST /api/revisions/{revisionId}/restore

await fetch(`http://localhost:8080/api/revisions/${revisionId}/restore`, {
    method: 'POST',
    headers: {
        'X-CSRF-Token': csrfToken
    },
    credentials: 'include'
});

Delete Revision

DELETE /api/revisions/{revisionId}

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

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

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

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

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

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

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

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

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

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

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

await fetch('/api/sync/now', {
    method: 'POST',
    headers: {
        'X-CSRF-Token': csrfToken
    },
    credentials: 'include'
});

Get Sync Log

GET /api/sync/log

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

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:

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}

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}

const weekNote = await fetch(`/api/calendar/weeks/2024-01-15`, {
    credentials: 'include'
}).then(r => r.json());

Get Month Note

GET /api/calendar/months/{month}

const monthNote = await fetch(`/api/calendar/months/2024-01`, {
    credentials: 'include'
}).then(r => r.json());

Inbox Note

GET /api/inbox/{date}

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:

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:

const similarNotes = await fetch(`/api/notes/${noteId}/similar`, {
    credentials: 'include'
}).then(r => r.json());

Options and Configuration

Get All Options

GET /api/options

const options = await fetch('/api/options', {
    credentials: 'include'
}).then(r => r.json());

Update Option

PUT /api/options/{optionName}

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

const preferences = await fetch('/api/options/user', {
    credentials: 'include'
}).then(r => r.json());

Database Operations

Backup Database

POST /api/database/backup

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

await fetch('/api/database/vacuum', {
    method: 'POST',
    headers: {
        'X-CSRF-Token': csrfToken
    },
    credentials: 'include'
});

Get Database Info

GET /api/database/info

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:

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

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:

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

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

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

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

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

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', '<p>Content</p>');
const searchResults = await explorer.searchInSubtree('keyword');

Building a Task Management System

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: '<h1>Task Management</h1>'
            }),
            credentials: 'include'
        });
        
        const { note } = await response.json();
        
        // Add taskRoot label
        await this.addLabel(note.noteId, 'taskRoot');
        
        return note.noteId;
    }
    
    async createTask(title, description, priority = 'medium', dueDate = null) {
        // Create task note
        const response = await fetch(`/api/notes/${this.taskRootId}/children`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-Token': this.csrfToken
            },
            body: JSON.stringify({
                title,
                type: 'text',
                content: `<h2>${title}</h2><p>${description}</p>`
            }),
            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 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: