Use when interacting with a Trilium Notes server via the ETAPI REST API - creating, reading, updating, searching, or deleting notes, branches, attributes, attachments, day/week/month notes; obtaining auth tokens; or scripting Trilium operations from the shell. Triggers on mentions of Trilium, ETAPI, noteId, branchId, or any /etapi/ URL.

Install

openclaw skills install trilium-etapi

Trilium ETAPI

Overview

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:

  • Note — a content unit (HTML/code/file/image/...), identified by noteId
  • Branch — the parent-child relationship between two notes (the same note can be cloned under multiple parents)
  • Attribute — a label or relation attached to a note
  • Attachment — a binary or text payload owned by a note

When to Use

  • Bulk-create or import notes via script (daily notes, inbox, capture)
  • Push content into Trilium from external systems (RSS, email, webhooks)
  • Search/export Trilium content for consumption by other tools
  • Trigger server-side database backups, export subtrees
  • Debug ETAPI integrations (clients like trilium-py / trilium-client just wrap this API)

Do not use for scripts running inside Trilium — use the frontend/backend Script API directly, no HTTP needed.

Setup

All examples below assume:

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:

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):

  1. Authorization: $TRILIUM_TOKEN — raw token (works on every version)
  2. Authorization: Bearer $TRILIUM_TOKEN — Bearer form (v0.93+)
  3. Authorization: Basic $(echo -n "etapi:$TRILIUM_TOKEN" | base64) — Basic auth (v0.56+)

Quick Reference

OperationMethodPath
Health / versionGET/etapi/app-info
Search notesGET/etapi/notes?search=...
Read note metadataGET/etapi/notes/{noteId}
Read note contentGET/etapi/notes/{noteId}/content
Write note contentPUT/etapi/notes/{noteId}/content (text/plain)
Create notePOST/etapi/create-note
Patch note metadataPATCH/etapi/notes/{noteId}
Delete noteDELETE/etapi/notes/{noteId}
Export subtree as ZIPGET/etapi/notes/{noteId}/export?format=html|markdown
Import ZIPPOST/etapi/notes/{noteId}/import
Create / move branchPOST/etapi/branches
Create attributePOST/etapi/attributes
Create attachmentPOST/etapi/attachments
Day 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.

Core Patterns (curl + jq)

All snippets below assume TRILIUM_URL and TRILIUM_TOKEN are exported.

1. Health check

curl -s "$TRILIUM_URL/etapi/app-info" -H "Authorization: $TRILIUM_TOKEN" | jq

2. Create a text note

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.

3. Replace note content

Content lives on a separate endpoint and the body is text/plain even when the content is HTML:

curl -sX PUT "$TRILIUM_URL/etapi/notes/$NOTE_ID/content" \
  -H "Authorization: $TRILIUM_TOKEN" \
  -H 'Content-Type: text/plain' \
  --data-binary @body.html

4. Search

# 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 = "...".

5. Tag a note

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

6. Day note (auto-created on demand)

TODAY=$(date +%F)
curl -s "$TRILIUM_URL/etapi/calendar/days/$TODAY" \
  -H "Authorization: $TRILIUM_TOKEN" | jq -r .noteId

7. Clone a note to another location (branch)

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

8. Export a subtree as ZIP

curl -s "$TRILIUM_URL/etapi/notes/$NOTE_ID/export?format=markdown" \
  -H "Authorization: $TRILIUM_TOKEN" -o subtree.zip
# Whole document: use noteId="root"

9. Trigger a server-side backup

curl -sX PUT "$TRILIUM_URL/etapi/backup/now" \
  -H "Authorization: $TRILIUM_TOKEN"
# Writes to dataDirectory/backup/backup-now.db

Common Pitfalls

  • Don't add a 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.
  • Deleting the last branch of a note also deletes the note. Watch out when DELETE-ing branches.
  • 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.
  • EntityId pattern is [a-zA-Z0-9_]{4,32}. root is the special root noteId.
  • Error responses are always {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.

Note Types

typeUsemime required?
textRich text (HTML)no
codeSource codeyes (e.g. text/x-python)
fileBinary fileyes
imageImageyes (e.g. image/png)
searchSaved searchno
bookFolder-style containerno
relationMapRelation mapno
renderCustom rendererno

You may also see these on read: noteMap, mermaid, webView, shortcut, doc, contentWidget, launcher.

Workflow: Append to today's day note

A typical inbox / capture flow — append arbitrary text to today's day note:

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"

When You Need More