46 KiB
Vendored
Internal API Reference
Table of Contents
- Introduction
- Authentication and Session Management
- Core API Endpoints
- WebSocket Real-time Updates
- File Operations
- Import/Export Operations
- Synchronization API
- When to Use Internal vs ETAPI
- 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
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: