Files
MindSift/Doku/api/rest.md
T
sandrodachandClaude Opus 5.5 68bc4d279c
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m20s
fix(workspace): hidden bullet actions on phones, no second extract, notice layout
- The floating bullet actions only react to hover on devices with a real
  pointer; a tap on a phone made the invisible buttons clickable.
- A bullet that is only a note link (plus hashtags) can no longer be
  extracted: button and /extract are hidden, the server answers 400.
- Extract notice on phones sits above the bottom navigation, text on
  top (two lines max) with the actions below.
- Release v0.26.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 19:56:32 +01:00

23 KiB
Raw Blame History

REST-API

Basis: /api/v1. Antworten sind JSON; Fehler haben die Form {"error": "Text"}. Authentifizierung über Authorization: Bearer <JWT>. API-Keys (Bearer mindsift_sec_… bzw. Header X-API-Key) liefern nur Daten ein, über POST /inbox und POST /webhooks/ingest, und nur für die Ziele ihrer Scopes: webhook:triage (Inbox), webhook:task (Aufgaben), webhook:list (Listeneinträge). Alles andere, auch Live-Streams, antwortet mit 403 bzw. 401. Zeitstempel sind ISO-8601 (UTC). Anfragekörper sind auf 2 MB begrenzt (Notiz-Sync 8 MB, Importe 101 MB). Im Demo-Modus antworten Endpunkte, die etwas verschicken, Server-Einstellungen ändern oder die gemeinsamen Demo-Konten verändern würden, mit 403 „In der Demo deaktiviert“. Quelle der Wahrheit sind die Routen in server.go.

Öffentlich

Methode Pfad Zweck
GET /health Lebenszeichen
GET /auth/status needsSetup, allowRegistration, instanceName, demoMode, sharingMode, legal (imprintUrl, hasImprintContent, privacyUrl, hasPrivacyContent, customLinkLabel, customLinkUrl)
GET /legal/{imprint|privacy} Hinterlegter Markdown-Text (content), ohne Anmeldung; 404, wenn keiner gesetzt ist
POST /share-target PWA Web Share Target Fallback: Weiterleitung auf / (303 See Other), falls Service Worker noch inaktiv
POST /auth/setup Erstes Administratorkonto (email, password, displayName, instanceName, allowRegistration, sharingMode, seedSampleData); nur solange es keine Konten gibt
POST /auth/verify-email Link aus der Bestätigungs-E-Mail (token): schaltet das Konto frei oder reicht es an die Freigabe weiter ({"verified","pending","message"})
POST /auth/resend-verification Bestätigungslink erneut schicken (email); antwortet immer gleich, höchstens alle 2 Minuten pro Konto
POST /auth/register Registrierung (email, password, displayName, inviteCode); mit Code immer, sonst je nach Registrierungsmodus. 201 mit Token oder 202 {"pending":true,"verify","message"}, wenn erst die E-Mail-Adresse bestätigt (verify) und/oder ein Admin freigeben muss (Login bis dahin 403); 429 nach 20 Versuchen pro Adresse in 15 Minuten oder bei mehr als 50 wartenden Konten
POST /auth/login Anmeldung (login/email/username, password) → {token, user}
POST /auth/forgot-password {email} → immer {ok: true}; Mail mit Link nur, wenn das Konto existiert. 503, wenn SMTP oder BASE_URL fehlen; 429 nach 10 Anfragen pro Adresse in 15 Minuten
POST /auth/reset-password {token, password} (mind. 8 Zeichen) → neues Passwort, alle Sitzungen beendet; 400 bei ungültigem oder abgelaufenem Link
POST /auth/quickstart Immer 403 (Gastzugang deaktiviert)
POST /webhooks/ingest Einliefern per Webhook, Key als X-API-Key (empfohlen), Bearer oder ?token= (in der URL nur API-Keys, keine Sitzungs-Tokens). Body: title/content/text/message, priority, source, tags, dueDate; target (oder type): triage (Standard, Inbox), task (optional taskListId), list_item (listId, category, quantity). Klartext-Body wird als Inhalt genommen. 403 ohne passenden Scope oder ohne Schreibrecht auf die Liste
GET /ws, /ws/spaces/{id}, /ws/lists/{id} WebSocket, siehe websocket.md; ?ticket= oder Authorization-Header
GET /events SSE-Fallback; ?ticket= oder Authorization-Header

Konto und Allgemeines (angemeldet)

Methode Pfad Zweck
GET /auth/me Benutzer und Kennzahlen
POST /events/rooms SSE-Raum beitreten/verlassen (sseId, room, action); nur eigene sseId, nur Räume mit Lesezugriff
POST /auth/stream-ticket Einmal-Ticket (60 s) für WebSocket/SSE
POST /voice/transcribe Nur im Server-Modus der Spracherkennung: multipart/form-data mit file (Aufnahme, z. B. WebM/Opus oder MP4/AAC) → {text}. 404 ohne STT_URL, 413 zu groß, 429 Stundenlimit, 503 ausgelastet, 502 Whisper-Fehler (Details im Log)
POST /auth/logout Sitzung dieses Geräts beenden
GET /auth/sessions Angemeldete Geräte: [{id, createdAt, lastUsedAt, userAgent, ip, current}]
DELETE /auth/sessions/{id} Ein Gerät abmelden; 404 bei fremder oder unbekannter Sitzung
POST /auth/revoke-all Alle Sitzungen beenden (token_version +1)
PATCH /auth/settings displayName, email, password, avatarUrl, alarmChannels, alarmEmail, discordWebhookUrl, customWebhookUrl, notifyBeforeMinutes. notificationSettings (paused, includeOwnWatched, events.{due,shared,taskDone,watched}.{email,discord}, dailySummary.{enabled,time,email,discord}) für Benachrichtigungen. Neue email oder password nur mit currentPassword (403 bei falschem, 429 bei Login-Sperre); nach neuem Passwort sind andere Geräte abgemeldet, das neue Token kommt im Header X-Session-Token
POST /auth/alarms/test Test-Alarm (channel: email/discord/webhook, optional target)
GET /users/directory, /collaborators Personen, die man sehen und mit denen man teilen kann (je nach sharingMode, sharing-and-permissions.md)
GET /stats Zähler fürs Dashboard (u. a. inboxCount, tasksOpenCount, listItemsOpenCount = offene Einträge aller sichtbaren Listen)
GET /search?q= Globale FTS5-Suche in zugänglichen Notizen (Titel & Blöcke), Aufgaben und Listen → {thoughts, documents, listItems, hits}
GET /export Datenexport als JSON; mit ?format=zip dieselben Daten als mindsift-backup.json in einer ZIP (so lädt die Oberfläche das Backup)
GET /export/markdown Obsidian-kompatibles ZIP (mindsift-vault-DATUM.zip), direkt gestreamt: Notizen als .md mit YAML-Frontmatter in der Ordnerstruktur des Nutzers (inkl. geteilter), Aufgaben/<Liste>.md im Format des Tasks-Plugins (📅, ⏫/🔺), Listen/<Liste>.md
GET/POST /themes Eigene CSS-Themes; anlegen oder mit id ändern (name, css, max. 200 KB)
DELETE /themes/{id} Eigenes Theme löschen
POST /import/mindsift, /import/obsidian Import (Multipart-Feld file, max. 100 MB): MindSift-JSON/-Archiv bzw. Obsidian-Vault-ZIP; ?preview=1 liefert nur die Zusammenfassung (folders, notes, taskLists, tasks, lists, listItems, inbox, skipped)
— /import/mindsift?mode=restore Wiederherstellen: löscht vor dem Import alle eigenen Notizen, Ordner, Aufgaben, Gedanken und Listen (erst nachdem die Datei vollständig gelesen und geprüft ist)
DELETE /user/data Alle eigenen Daten löschen, Konto bleibt ({"password"} nötig)
DELETE /user/account Eigenes Konto mit allen Daten löschen ({"password"} nötig); 403 für den letzten Server-Admin. /auth/me liefert dafür isLastAdmin
GET /backups, /backups/{name} Eigene automatische Server-Backups auflisten bzw. herunterladen
POST /backups/{name}/restore Server-Backup einspielen: mode=merge oder restore (gewählte Bereiche vorher leeren), sections=notes,tasks,lists,inbox,themes, preview=1
GET/POST /keys API-Keys auflisten bzw. anlegen (Klartext nur bei Erstellung); POST mit name und scopes (Standard ["webhook:triage"])
DELETE /keys/{id} API-Key löschen

Globale Suche (GET /api/v1/search?q=)

Führt eine Volltextsuche über SQLite FTS5 mit BM25-Relevanz-Ranking aus.

  • Durchsucht Aufgaben/Gedanken (Inhalt, Notizen, Unteraufgaben), Notizen (Titel sowie Inhalte aktiver Blöcke) und Listeneinträge.
  • Beachtet strikt alle Zugriffsregeln: Nur eigene Gedanken/Aufgaben und solche in zugänglichen Aufgabenlisten; nur eigene und geteilte Notizen (direkt, über Gruppen oder freigegebene Ordner); nur eigene und geteilte Listen.
  • Titel-Treffer werden höher gewichtet als Treffer im Fließtext.
  • Liefert rückwärtskompatible Collections (thoughts, documents, listItems) sowie strukturierte hits mit Snippet und <mark>-Hervorhebung:
    {
      "thoughts": [...],
      "documents": [...],
      "listItems": [...],
      "hits": [
        {
          "type": "document",
          "id": "blk_123",
          "title": "Projektplanung",
          "snippet": "... wichtige Notiz zum <mark>Release</mark> ...",
          "docId": "doc_abc",
          "blockId": "blk_123"
        }
      ]
    }
    

Gedanken, Triage, Aufgaben

Methode Pfad Zweck
POST /inbox Schnellerfassung. Body: content, priority, source, dueDate, target (inbox/task/list_item/document), targetListId, taskListId (snake_case-Varianten werden akzeptiert)
GET /thoughts?status= Gedanken (inbox, kept, converted, discarded, all)
GET/PATCH/DELETE /thoughts/{id} Einzelabruf; Teiländerung (content, isCompleted, dueDate, priority, taskListId, kanbanStatus, notes, subtasks, tags, assignees, recurrence = daily/weekly/monthly/yearly/leer); Löschen. assignees ersetzt die Zuweisungen; nur Personen mit Zugriff auf die Liste, sonst 403
DELETE /thoughts/discarded/purge Papierkorb leeren
GET /triage/deck Posteingang (max. 100)
PATCH /triage/{id} action: keep/discard/restore; mit convertTo: task/document/list_item
GET /tasks?taskListId= Aufgaben (optional einer Liste)
POST /tasks/reorder, /task-lists/{id}/tasks/reorder Reihenfolge/Spalte ändern (items[{id, sortOrder, kanbanStatus}])
POST /tasks/{id}/unlink Verknüpfung zu Notiz-Block lösen
POST/DELETE /tasks/{id}/watch Aufgabe beobachten bzw. nicht mehr → {watching}; 404 ohne Zugriff. Aufgaben-Antworten tragen isWatching

Aufgabenlisten

Methode Pfad Zweck
GET/POST /task-lists Listen (permission je Liste: owner/editor/viewer); anlegen (title, icon, color, teamId)
PUT/DELETE /task-lists/{id} Ändern (title, icon, color, teamId, columns) – nicht für Leser; Löschen nur Eigentümer
GET /task-lists/{id}/shares Freigaben
POST /task-lists/{id}/shares Nur Eigentümer. Body: targetType (team/user), targetId, permission (editor/viewer, Standard editor). Erneutes Senden für dasselbe Ziel ändert die Stufe
DELETE /task-lists/{id}/shares/{shareId} Nur Eigentümer

Schreibzugriffe von Lesern auf Listen oder deren Aufgaben liefern 403 („Keine Schreibrechte für diese Aufgabenliste (nur Lesen)“).

Workspace

Methode Pfad Zweck
GET/POST/DELETE /folders Ordnerliste, anlegen (path), löschen
POST /folders/move sourcePath, targetParentPath
GET/POST /workspace/folders/shared Geteilte Ordner; anlegen (name, mountPath)
GET/DELETE /workspace/folders/shared/{id} Abruf; Löschen
PUT /workspace/folders/shared/{id}/mount Eigenen Einhängepfad ändern (mountPath)
GET/POST /workspace/folders/shared/{id}/shares Freigaben geteilter Ordner (permission: read/write)
DELETE /workspace/folders/shared/{id}/shares/{shareId} Freigabe eines geteilten Ordners entfernen
GET /workspace/tags, /workspace/tags/{tag} Tag-Übersicht und -Details
PUT /workspace/tags/{tag}/note Tag-Seite speichern (content)
GET/POST /documents Notizen auflisten; anlegen (title, icon, folderPath, blocks, tags, isPinned)
GET/PUT/DELETE /documents/{id} Abruf; Ändern von title, icon, folderPath, isPinned (Blöcke → 400, sie laufen über /docs/{id}/sync); Löschen = in den Papierkorb (Eigentümer) bzw. Freigabe verlassen (andere)
GET/DELETE /documents/trash Papierkorb auflisten (id, title, icon, folderPath, deletedAt, isOwner) bzw. leeren
GET /documents/{id}/backlinks Notizen mit [[Titel]]-Link auf diese Notiz (nur sichtbare, ohne Papierkorb): docId, title, icon, folderPath, bis zu 5 blocks (id, content), more
POST /documents/{id}/restore Aus dem Papierkorb zurückholen (fehlender Ordner wird neu angelegt)
DELETE /documents/{id}/permanent Notiz im Papierkorb endgültig löschen
PUT /documents/{id}/move Ordner wechseln (folderPath)
POST /documents/{id}/blocks/{blockId}/enhanced-task Aus einem Block eine verknüpfte Aufgabe machen (content, checked)
GET/POST /documents/{id}/shares Notiz-Freigaben (userId/email/teamId, permission: read/write)
DELETE /documents/{id}/shares/{userId} Notiz-Freigabe entfernen
POST /documents/{id}/extract {blockId, clientId}: legt aus den Unterpunkten des Punkts eine eigene Notiz an (Titel = Text ohne Hashtags, gleicher Ordner, Freigaben übernommen, fremder Besitzer bekommt Schreibrecht) → {document, link}. Die Ursprungsnotiz ändert der Server nicht; der Editor ersetzt den Ast selbst durch link (normale eigene Änderung, Strg+Z möglich). Schreibrecht nötig; 409, wenn jemand anderes im Ast schreibt; 400, wenn der Punkt nur noch ein Link auf eine Notiz ist (ggf. mit Hashtags)
GET /push/key {enabled, publicKey} – öffentlicher VAPID-Schlüssel für pushManager.subscribe (in der Demo enabled: false)
GET /push/devices Eigene Geräte mit Push → {devices: [{id, endpoint, device, createdAt}]}
POST /push/subscribe {endpoint, keys: {p256dh, auth}, device} – Gerät anmelden (nur https-Endpunkte)
POST /push/unsubscribe {endpoint} – Gerät abmelden
POST /push/test Test-Benachrichtigung an alle eigenen Geräte → {devices}; 400 ohne Gerät
GET /journal Tage des eigenen Journals, neueste zuerst → {days: [{date, docId, empty}]} (empty: Notiz noch ohne Text; Tage im Papierkorb fehlen)
POST /journal/{date} Notiz des Tages (JJJJ-MM-TT) öffnen; gibt es noch keine, wird sie angelegt (Titel z. B. „Freitag, 30. Oktober 2026“, Icon 📅) → Dokument, 201 bei Neuanlage. Die Journal-Notizen sind normale Notizen (Teilen, Suche, Sync)
POST /documents/{id}/extract/undo Legt die ausgelagerte Notiz {id} wieder in den Papierkorb (nur ihr Ersteller)
POST /documents/{id}/merge {sourceId}: hängt die (eigene) Quellnotiz als Überschrift mit Unterpunkten an diese Notiz an, Quelle in den Papierkorb → {targetId, sourceId, sourceTitle, headingId}
POST /documents/{id}/merge/undo {sourceId, headingId}: Abschnitt wieder entfernen, Quelle wiederherstellen
POST /documents/{id}/media Bild oder Audio zu einer Notiz hochladen (multipart/form-data, Feld file; Schreibrecht nötig) → {id, kind, mime, size, url}. 415 falsches Format, 413 zu groß oder Kontingent voll
POST /documents/{id}/media/fetch Externes Bild kopieren: {url} (nur https, keine internen Adressen; nicht in der Demo) → wie oben
GET /media/{id} Datei abrufen (Leserecht auf eine Notiz, die sie zeigt), mit Range-Anfragen; sonst 404
GET /link-preview?url=https://… Vorschau einer Web-Seite (title, description, siteName, hasImage, hasIcon, ok, key), vom Server geholt und 7 Tage zwischengespeichert; 422 ohne https, 403 in der Demo, 429 zu viele Abrufe
GET /link-preview/{key}/image bzw. /icon Bild bzw. Icon einer Vorschau

Der Inhalt einer Notiz (Blöcke) wird ausschließlich über POST /docs/{id}/sync geändert. POST /documents nimmt beim Anlegen weiterhin eine Start-Blockliste an; sie wird intern in Operationen umgerechnet.

Gemeinsames Bearbeiten von Notizen

Konzept und Regeln: modules/collaboration.md. Zugriff über die Rolle owner/editor/viewer (Store.DocRole); ohne Rolle 404.

Methode Pfad Zweck
GET /docs/{id}/snapshot Kompletter Blockbaum inkl. Grabsteinen, title, titleVersion, headSeq, role, shared
POST /docs/{id}/sync Ein Rundlauf: eigene Operationen senden, Leases halten/abgeben, Log ab lastSeq abholen
GET /docs/{id}/changes?since=N&wait=S Long-Polling: antwortet sofort, wenn headSeq > N, sonst bei der nächsten Änderung (auch Leases/Präsenz) oder nach höchstens 25 s. Antwort {headSeq, changed}; Daten holt der Client danach per /sync
POST /docs/{id}/leave Sitzung beenden, alle Leases des Clients freigeben ({clientId})

/sync-Anfrage:

{
  "clientId": "tab-7f3…",
  "lastSeq": 412,
  "ops": [
    { "opId": "c1a…", "kind": "set_content", "blockId": "b9…", "baseVersion": 7, "content": "Milch und Brot" },
    { "opId": "c1b…", "kind": "insert", "blockId": "bA…", "parentId": "b2…", "orderKey": "a0V", "type": "bullet", "content": "" }
  ],
  "leases": { "hold": ["b9…", "bA…"], "release": [] }
}
  • Operationsarten: insert, set_content, set_props (patch: type, checked, collapsed, lang, clearConflict), move, delete, restore (ids), set_title (baseVersion, title).
  • leases.hold ist die vollständige Liste der Blöcke, die der Client halten will; alle anderen Leases des Clients werden freigegeben. Neu eingefügte Blöcke und Konfliktkopien bekommt der Autor automatisch.
  • Höchstens 500 Operationen pro Anfrage. Leser dürfen keine Operationen senden (403), aber abfragen.
  • skipLog: true liefert kein Log zurück; gedacht für einzelne Änderungen ohne geöffneten Editor (z. B. Abhaken in der To-Do-Übersicht).

Antwort: results (je Operation status applied/conflict/rejected, ggf. conflictBlockId, version, seq), ops (Log-Einträge {seq, opId, userId, clientId, effect} ab lastSeq, höchstens 1000; hasMore bei mehr), headSeq, resync (Snapshot neu laden: der Client ist dem Server voraus, oder sein lastSeq liegt vor der verdichteten Grenze log_floor; die gesendeten Operationen werden trotzdem angewendet), role, shared, sessions, presence, leases (granted, denied, others).

Wiederholte Operationen (gleiche opId) werden nicht erneut angewendet; die Antwort enthält das gespeicherte Ergebnis.

Geteilte Listen

Methode Pfad Zweck
GET/POST /lists Listen; anlegen (title, icon, listType)
GET/DELETE /lists/{id} Abruf mit Einträgen; Löschen
GET/POST /lists/{id}/shares Freigaben abrufen bzw. anlegen (targetType, targetId, role: editor/viewer)
DELETE /lists/{id}/shares/{shareId} Freigabe entfernen
POST /lists/{id}/items Eintrag (content, category, quantity)
PATCH /lists/{id}/items/{itemId} Abhaken (isCompleted)
PUT /lists/{id}/items/{itemId} Ändern (content, category, quantity)
DELETE /lists/{id}/items/{itemId} Löschen
POST /lists/{id}/items/{itemId}/move In eine andere Liste verschieben (targetListId); Bearbeitungsrecht in beiden Listen nötig, der Eintrag landet oben
POST/PUT /lists/{id}/items/reorder Reihenfolge/Kategorie (items[{id, sortOrder, category, content}])
POST /lists/{id}/clear-completed Erledigte entfernen

Gruppen

Methode Pfad Zweck
GET/POST /teams Eigene Gruppen; anlegen (name, description) – Ersteller wird Gruppen-Admin, die Gruppe gehört zu seiner Organisation; im Modus private gesperrt
PUT/DELETE /teams/{id} Nur Gruppen-Admin
POST /teams/{id}/members Mitglied hinzufügen (userId oder email, role); nur sichtbare Personen, sonst 404
PUT/DELETE /teams/{id}/members/{userId} Rolle ändern (admin/member); entfernen

Administration (role = admin)

Methode Pfad Zweck
GET/POST /admin/users Benutzer auflisten, anlegen (email, password, displayName, role, optional organizationId)
PUT /admin/users/{id}/organization Organisation setzen (organizationId, leer = keine); entzieht Freigaben, Gruppen und Zuweisungen der alten
GET/POST /admin/organizations Organisationen mit memberCount; anlegen (name)
PUT/DELETE /admin/organizations/{id} Umbenennen (name); löschen (Mitglieder sind danach ohne Organisation)
PATCH/DELETE /admin/users/{id} displayName, email, role, isActive, discordWebhookUrl; löschen (bei wartenden Konten = ablehnen). GET /admin/users markiert wartende Konten mit pendingApproval
POST /admin/users/{id}/approve Wartendes Konto freigeben; die Person bekommt eine E-Mail (409, wenn es nicht wartet)
PUT /admin/users/{id}/password Passwort zurücksetzen
GET/POST /admin/invites Einladungen auflisten bzw. anlegen (email, role, organizationId, validDays, maxUses 1–1000; mehrfach verwendbare nicht an eine E-Mail gebunden). Antwort mit maxUses/useCount. Als Link: /?invite=<CODE> öffnet die Registrierung mit eingetragenem Code
POST /admin/invites/{id}/send Einladung per E-Mail (email); bindet einmalige Codes an diese Adresse; braucht SMTP
DELETE /admin/invites/{id} Einladung widerrufen/löschen
GET/PUT /admin/settings instanceName, allowRegistration, sharingMode (private/organizations/open), SMTP-Felder; ein Moduswechsel bereinigt Freigaben. smtpPass kommt nie zurück (smtpPassSet zeigt, ob eines gespeichert ist); leer = unverändert
GET /admin/settings/sharing-impact?mode= Probelauf: wie viele Freigaben und Gruppen-Mitgliedschaften ein Wechsel entfernen würde (affected)
POST /admin/smtp/test Testmail (toEmail)
GET /admin/voice Aktive Spracherkennung (mode: server/browser/off), Einstellungen aus STT_* ohne Schlüssel, Grenzen und im Server-Modus ein frischer Zustand (status.reachable, status.modelReady)
GET/POST /admin/backups Datenbank-Abzüge auflisten bzw. sofort sichern (Abzug + alle Benutzer-Backups)
GET /admin/backups/{name} Datenbank-Abzug herunterladen
DELETE /admin/backups/{name} Datenbank-Abzug löschen (gibt die verbleibende Liste zurück; nicht in der Demo)

Beispiele

# Gedanken per API-Key erfassen
curl -X POST https://mindsift.example.com/api/v1/webhooks/ingest \
  -H "X-API-Key: mindsift_sec_…" -H "Content-Type: application/json" \
  -d '{"title":"Milch kaufen #einkauf","priority":"high","dueDate":"2026-11-01"}'

# Einkaufsliste per API-Key ergänzen (Scope webhook:list)
curl -X POST https://mindsift.example.com/api/v1/webhooks/ingest \
  -H "X-API-Key: mindsift_sec_…" -H "Content-Type: application/json" \
  -d '{"target":"list_item","listId":"lst_…","content":"Hafermilch","quantity":"2"}'

# Aufgabenliste nur lesend freigeben
curl -X POST https://mindsift.example.com/api/v1/task-lists/tkl_…/shares \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"targetType":"user","targetId":"usr_…","permission":"viewer"}'