ETAPI is the external REST API for Trilium Notes (Trilium ≥ 0.50). All requests require token auth. Resources are addressed by 12-char IDs: noteId, branchId, attributeId, attachmentId.
Core concepts:
noteIdDo not use for scripts running inside Trilium — use the frontend/backend Script API directly, no HTTP needed.
All examples below assume:
bash
export TRILIUM_URL="http://localhost:8080" # no trailing /etapi
export TRILIUM_TOKEN="<generate via Trilium → Options → ETAPI>"
If you only have a password (and the server allows password login), exchange it for a token:
bash
curl -sX POST "$TRILIUM_URL/etapi/auth/login" \
-H 'Content-Type: application/json' \
-d '{"password":"YOUR_PASSWORD"}' | jq -r .authToken
Three auth styles (pick one, in the Authorization header):
Authorization: $TRILIUM_TOKEN — raw token (works on every version)Authorization: Bearer $TRILIUM_TOKEN — Bearer form (v0.93+)Authorization: Basic $(echo -n "etapi:$TRILIUM_TOKEN" | base64) — Basic auth (v0.56+)OperationMethodPathHealth / versionGET/etapi/app-infoSearch notesGET/etapi/notes?search=...Read note metadataGET/etapi/notes/{noteId}Read note contentGET/etapi/notes/{noteId}/contentWrite note contentPUT/etapi/notes/{noteId}/content (text/plain)Create notePOST/etapi/create-notePatch note metadataPATCH/etapi/notes/{noteId}Delete noteDELETE/etapi/notes/{noteId}Export subtree as ZIPGET/etapi/notes/{noteId}/export?format=html|markdownImport ZIPPOST/etapi/notes/{noteId}/importCreate / move branchPOST/etapi/branchesCreate attributePOST/etapi/attributesCreate attachmentPOST/etapi/attachmentsDay note (auto-create)GET/etapi/calendar/days/{YYYY-MM-DD}InboxGET/etapi/inbox/{YYYY-MM-DD}Trigger DB backupPUT/etapi/backup/{name}
Full endpoint, parameter, and schema reference: api-reference.md.
All snippets below assume TRILIUM_URL and TRILIUM_TOKEN are exported.
bash
curl -s "$TRILIUM_URL/etapi/app-info" -H "Authorization: $TRILIUM_TOKEN" | jq
bash
curl -sX POST "$TRILIUM_URL/etapi/create-note" \
-H "Authorization: $TRILIUM_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"parentNoteId": "root",
"title": "Hello from ETAPI",
"type": "text",
"content": "<p>Created via curl</p>"
}' | jq '{noteId: .note.noteId, branchId: .branch.branchId}'
Returns NoteWithBranch: the new note plus the branch mounting it.
Content lives on a separate endpoint and the body is text/plain even when the content is HTML:
bash
curl -sX PUT "$TRILIUM_URL/etapi/notes/$NOTE_ID/content" \
-H "Authorization: $TRILIUM_TOKEN" \
-H 'Content-Type: text/plain' \
--data-binary @body.html
bash
# fulltext + label
curl -sG "$TRILIUM_URL/etapi/notes" \
-H "Authorization: $TRILIUM_TOKEN" \
--data-urlencode 'search=tolkien #book' \
--data-urlencode 'limit=10' | jq '.results[] | {noteId, title}'
Search syntax matches the Trilium UI search bar. Common forms: #tag, #tag=value, note.content *= "...", ~relation.title = "...".
bash
curl -sX POST "$TRILIUM_URL/etapi/attributes" \
-H "Authorization: $TRILIUM_TOKEN" \
-H 'Content-Type: application/json' \
-d "{
\"noteId\": \"$NOTE_ID\",
\"type\": \"label\",
\"name\": \"book\",
\"value\": \"\",
\"isInheritable\": false
}"
bash
TODAY=$(date +%F)
curl -s "$TRILIUM_URL/etapi/calendar/days/$TODAY" \
-H "Authorization: $TRILIUM_TOKEN" | jq -r .noteId
bash
curl -sX POST "$TRILIUM_URL/etapi/branches" \
-H "Authorization: $TRILIUM_TOKEN" \
-H 'Content-Type: application/json' \
-d "{
\"noteId\": \"$CHILD_ID\",
\"parentNoteId\": \"$NEW_PARENT_ID\",
\"prefix\": \"\",
\"notePosition\": 100,
\"isExpanded\": false
}"
bash
curl -s "$TRILIUM_URL/etapi/notes/$NOTE_ID/export?format=markdown" \
-H "Authorization: $TRILIUM_TOKEN" -o subtree.zip
# Whole document: use noteId="root"
bash
curl -sX PUT "$TRILIUM_URL/etapi/backup/now" \
-H "Authorization: $TRILIUM_TOKEN"
# Writes to dataDirectory/backup/backup-now.db
Bearer prefix to Authorization unless you're on v0.93+ and explicitly want Bearer. The raw token form works on every version.PUT /notes/{id}/content body is text/plain (NOT text/html). The OpenAPI spec is explicit on this.PATCH /notes/{id} only patches a subset: title, type, mime, dateCreated, utcDateCreated. Use PUT .../content to change content.PATCH /branches/{id} only patches prefix and notePosition. To re-parent a note you must DELETE the branch and POST a new one.PATCH /attributes/{id}: labels can only patch value and position; relations can only patch position. Anything else means delete + recreate.notePosition defaults to step 10 (10/20/30...). To insert in front, use 5; to push to the end, use a large value like 1000000. After bulk reorders, call POST /refresh-note-ordering/{parentNoteId} so connected clients refresh.[a-zA-Z0-9_]{4,32}. root is the special root noteId.{status, code, message}. Branch on the stable code constant (e.g. NOTE_IS_PROTECTED), not on the human-readable message./auth/login is rate-limited. Too many failures returns 429 and temporarily blacklists the client IP.typeUsemime required?textRich text (HTML)nocodeSource codeyes (e.g. text/x-python)fileBinary fileyesimageImageyes (e.g. image/png)searchSaved searchnobookFolder-style containernorelationMapRelation mapnorenderCustom rendererno
You may also see these on read: noteMap, mermaid, webView, shortcut, doc, contentWidget, launcher.
A typical inbox / capture flow — append arbitrary text to today's day note:
bash
TODAY=$(date +%F)
NOTE_ID=$(curl -s "$TRILIUM_URL/etapi/calendar/days/$TODAY" \
-H "Authorization: $TRILIUM_TOKEN" | jq -r .noteId)
OLD=$(curl -s "$TRILIUM_URL/etapi/notes/$NOTE_ID/content" \
-H "Authorization: $TRILIUM_TOKEN")
NEW="${OLD}<p>$(date +%H:%M) — $1</p>"
curl -sX PUT "$TRILIUM_URL/etapi/notes/$NOTE_ID/content" \
-H "Authorization: $TRILIUM_TOKEN" \
-H 'Content-Type: text/plain' \
--data-binary "$NEW"