SecretNotes – API
Eine Notiz pro Ableton-Set, pro Nutzer – und auf Wunsch mit anderen geteilt.
Was ist das? SecretNotes speichert zu jedem Ableton-Set genau eine Notiz. Der Name der Notiz ist der Name des Sets. Jeder Nutzer hat seine eigenen Notizen und kann einzelne davon anderen Nutzern zum Lesen oder Schreiben freigeben.
- Basis-URL:
https://notes.secret.vip - Alles ist JSON (UTF-8) – Anfragen wie Antworten.
- Fehler kommen immer als
{"error":"<code>"}mit passendem HTTP-Status (Tabelle).
In 3 Schritten loslegen
Token holen
Weg A – per API: POST /api/login mit einem device-Namen. Der Server legt dafür einen API-Key an und gibt den Token zurück (kein Cookie).
curl -s https://notes.secret.vip/api/login \
-H 'Content-Type: application/json' \
-d '{"email":"ich@example.com","password":"geheim123","device":"Ableton Live (MacBook)"}'
Antwort:
{
"token": "smn_…",
"user": {"id": 1, "email": "ich@example.com", "name": "Sebastian", "is_admin": false, "email_verified": true, "created_at": "2026-09-01T18:22:04.000Z"}
}
Weg B – auf der Website: Einloggen → Konto → API-Keys → neuen Key erzeugen. Der Token wird nur ein einziges Mal angezeigt – direkt kopieren.
Notiz lesen
Der Notizname steht im Pfad und muss URL-kodiert sein: Leerzeichen → %20. Aus Mein Track wird /api/notes/Mein%20Track.
curl -s https://notes.secret.vip/api/notes/Mein%20Track \
-H 'Authorization: Bearer smn_…'
Antwort (Notiz existiert noch nicht):
{
"name": "Mein Track",
"text": "",
"updated_at": null,
"exists": false,
"owner": {"email": "ich@example.com", "name": "Sebastian"},
"mine": true,
"permission": "owner"
}
Notiz schreiben
PUT ersetzt den ganzen Text und legt die Notiz an, falls sie fehlt.
curl -s -X PUT https://notes.secret.vip/api/notes/Mein%20Track \
-H 'Authorization: Bearer smn_…' \
-H 'Content-Type: application/json' \
-d '{"text":"Kick lauter\nVocals ab 1:30"}'
Antwort:
{
"name": "Mein Track",
"updated_at": "2026-09-03T10:15:30.123Z",
"owner": {"email": "ich@example.com", "name": "Sebastian"},
"permission": "owner"
}
Fertig. Für Automationen, die nur Zeilen anhängen wollen, ist PATCH mit append einfacher – dort ist kein Konfliktcheck nötig.
Authentifizierung
Zwei Wege, gleichwertig für alle /api/*-Routen:
- API-Key (Bearer) – für Geräte, Skripte, n8n und fremde Programme:
HeaderAuthorization: Bearer smn_<hex> - Session-Cookie – nur für die Website. Wird von
POST /api/loginohnedevicegesetzt (HttpOnly, 30 Tage gültig). Cookies werden cross-origin nicht mitgesendet – für alles außerhalb der Website also immer Bearer verwenden.
Ohne gültige Anmeldung antwortet der Server mit 401 {"error":"unauthorized"}.
Öffentlich (ohne Anmeldung erreichbar): GET /api/health, POST /api/login, POST /api/register, GET /api/register/info, POST /api/register/verify, POST /api/register/resend, GET /api/captcha, POST /api/password/forgot, POST /api/password/reset, GET /api/docs, GET /api/openapi.json.
CORS: Access-Control-Allow-Origin: * auf /api/*. Bearer-Aufrufe funktionieren aus jedem Browser oder Programm; OPTIONS-Preflight wird beantwortet.
Authorization-Header – nie als Query-Parameter, nie in Links, nie in Logs oder Screenshots. Ein kompromittierter Key lässt sich unter API-Keys jederzeit widerrufen.
Konten
Das Objekt user sieht überall gleich aus. Die E-Mail wird kleingeschrieben gespeichert.
{"id": 1, "email": "ich@example.com", "name": "Sebastian", "is_admin": false, "email_verified": true, "created_at": "2026-09-01T18:22:04.000Z"}
POST/api/registeröffentlich
Legt ein neues Konto an. Auf notes.secret.vip gilt Double-Opt-in: Es kommt eine Bestätigungs-Mail von hello@secret.vip mit einem 24 Stunden gültigen Link – erst nach dem Klick ist die Anmeldung möglich. Erstes Konto und Einladungen sind sofort angemeldet.
Body
email– Pflichtname– Pflicht, Anzeigenamepassword– Pflicht, mindestens 8 Zeicheninvite– optional, Einladungs-Token aus dem Einladungslinkcaptcha–{"id": "…", "answer": "…"}aus GET /api/captcha. Pflicht, außer beim allerersten Konto oder mit gültigeminvitewebsite– weglassen oder leer lassen. Honeypot gegen Bots: jeder Inhalt zählt als Fehlversuch und ergibt400 invalid_body
Erlaubt, wenn eine Bedingung zutrifft: noch kein Nutzer existiert (der erste wird Admin) oder invite gültig ist oder der Server offen ist (SMN_OPEN_REGISTRATION=1). Auf notes.secret.vip ist die Registrierung offen; pro IP sind 5 Registrierungen pro Stunde möglich.
Passwort: mindestens 8 Zeichen, kein Allerweltspasswort (Blockliste), nicht nur ein wiederholtes Zeichen, nicht gleich der E-Mail – sonst 400 weak_password.
Antwort 200
// Bestätigung ausstehend (Normalfall auf notes.secret.vip) – kein Cookie
{"user": {"id": 2, "email": "neu@example.com", "name": "Neu", "is_admin": false, "email_verified": false, "created_at": "2026-09-03T09:00:00.000Z"}, "verification_required": true, "verification_sent": true}
// erstes Konto, Einladung oder Server ohne E-Mail-Versand → sofort angemeldet, Set-Cookie
{"user": {"id": 2, "email": "neu@example.com", "name": "Neu", "is_admin": false, "email_verified": true, "created_at": "2026-09-03T09:00:00.000Z"}}
verification_sent: false heißt: Konto angelegt, aber die Mail konnte gerade nicht versendet werden – über POST /api/register/resend neu anfordern. Nicht bestätigte Konten werden nach 7 Tagen automatisch gelöscht.
Fehler
400 invalid_body 400 weak_password 400 invalid_invite 400 registration_closed 403 captcha_required (Captcha fehlt, falsch oder abgelaufen) 409 email_taken 429 too_many_attempts (mehr als 5 Registrierungen pro Stunde von dieser IP)
curl
curl -s https://notes.secret.vip/api/register \
-H 'Content-Type: application/json' \
-d '{"email":"neu@example.com","name":"Neu","password":"korrekt-pferd-batterie","captcha":{"id":"0f3a…","answer":"k7mpx"}}'
GET/api/register/infoöffentlich
Sagt, ob eine Registrierung gerade möglich ist – und prüft optional einen Einladungs-Token.
Query
invite– optional; wenn gesetzt, kommen zusätzlichinvite_validundinvite_emailzurück
Antwort 200
{"open": true, "first_user": false, "captcha_required": true, "password_reset": true, "email_verification": true, "min_password": 8}
// mit ?invite=<token>
{"open": true, "first_user": false, "captcha_required": true, "password_reset": true, "email_verification": true, "min_password": 8, "invite_valid": true, "invite_email": "neu@example.com"}
captcha_required: Registrierung braucht ein Captcha (nicht beim ersten Konto, nicht mit gültiger Einladung). password_reset: Passwort vergessen ist verfügbar (E-Mail-Versand eingerichtet). email_verification: neue Konten müssen die E-Mail-Adresse bestätigen (Bestätigungs-Mail); false nur, wenn kein E-Mail-Versand eingerichtet ist. min_password: Mindestlänge des Passworts.
invite_email ist null, wenn die Einladung nicht an eine bestimmte Adresse gebunden ist.
curl
curl -s 'https://notes.secret.vip/api/register/info?invite=a1b2c3…'
POST/api/register/verifyöffentlich
Bestätigt die E-Mail-Adresse mit dem Token aus der Bestätigungs-Mail und meldet das Konto per Session-Cookie an.
Body
token– Pflicht, aus dem Linkhttps://notes.secret.vip/?verify=<token>(24 Stunden gültig, einmal nutzbar)
Antwort 200 + Set-Cookie
{"ok": true, "user": {"id": 2, "email": "neu@example.com", "name": "Neu", "is_admin": false, "email_verified": true, "created_at": "2026-09-03T09:00:00.000Z"}}
Fehler
400 invalid_body 400 invalid_token (ungültig, abgelaufen oder schon benutzt – zählt als Fehlversuch) 429 too_many_attempts
curl
curl -s https://notes.secret.vip/api/register/verify \
-H 'Content-Type: application/json' \
-d '{"token":"ver_…"}'
POST/api/register/resendöffentlich
Schickt die Bestätigungs-Mail erneut (Absender hello@secret.vip).
Body
email– Pflichtcaptcha– Pflicht, aus GET /api/captcha
Die Antwort ist immer {"ok": true} – auch wenn es kein Konto gibt oder es schon bestätigt ist. Limits: 3 Mails pro E-Mail und Stunde, 10 Anfragen pro IP und Stunde.
Fehler
400 invalid_body 403 captcha_required 429 too_many_attempts 503 mail_unavailable
curl
curl -s https://notes.secret.vip/api/register/resend \
-H 'Content-Type: application/json' \
-d '{"email":"neu@example.com","captcha":{"id":"0f3a…","answer":"k7mpx"}}'
GET/api/captchaöffentlich
Holt einen Sicherheitscode als Bild (SVG). Wird auf dem Server erzeugt – kein Drittanbieter, kein Tracking.
Der Code ist einmal verwendbar und 10 Minuten gültig; Groß-/Kleinschreibung ist egal. Jede Prüfung verbraucht ihn – nach einer falschen Antwort also neu holen. Nötig bei Registrierung, Bestätigungs-Mail erneut senden, Passwort vergessen und beim Login, sobald der Server 403 captcha_required antwortet (nach 3 Fehlversuchen pro IP oder pro E-Mail).
Antwort 200
{"id": "0f3a…", "svg": "<svg xmlns=…>…</svg>", "ttl": 600}
Beim nächsten Aufruf mitschicken als "captcha": {"id": "0f3a…", "answer": "k7mpx"}.
Fehler
404 not_found (Captcha auf diesem Server abgeschaltet) 429 too_many_attempts (mehr als 30 Bilder in 10 Minuten pro IP)
curl
curl -s https://notes.secret.vip/api/captcha | jq -r .svg > captcha.svg # Bild ansehen, Code abtippen
POST/api/loginöffentlich
Meldet an – entweder als Website-Session (Cookie) oder als Gerät mit eigenem API-Key (Token).
Body
email– Pflichtpassword– Pflichtdevice– optional, z. B."Ableton Live (MacBook)". Wenn gesetzt: Es wird ein API-Key mit diesem Namen angelegt und alstokenzurückgegeben, kein Cookie.captcha– nur nötig, wenn der Server403 captcha_requiredgeantwortet hat (nach 3 Fehlversuchen pro IP oder pro E-Mail); Wert aus GET /api/captcha
Antwort 200
// ohne device → Session-Cookie wird gesetzt
{"user": {"id": 1, "email": "ich@example.com", "name": "Sebastian", "is_admin": false, "email_verified": true, "created_at": "2026-09-01T18:22:04.000Z"}}
// mit device → Token, kein Cookie
{"token": "smn_…", "user": {"id": 1, "email": "ich@example.com", "name": "Sebastian", "is_admin": false, "email_verified": true, "created_at": "2026-09-01T18:22:04.000Z"}}
Fehler
400 invalid_body 401 wrong_credentials 403 captcha_required (Captcha nötig, fehlt oder falsch – neues Bild holen) 403 email_unverified (E-Mail-Adresse noch nicht bestätigt – Link aus der Mail öffnen oder neu anfordern) 403 quota_exceeded (mit device: schon 50 Geräte-Zugänge) 429 too_many_attempts (10 Fehlversuche in 15 Minuten pro IP)
curl
curl -s https://notes.secret.vip/api/login \
-H 'Content-Type: application/json' \
-d '{"email":"ich@example.com","password":"geheim123","device":"Mein Skript"}'
POST/api/password/forgotöffentlich
Schickt einen Link zum Zurücksetzen des Passworts an die Konto-E-Mail (Absender hello@secret.vip).
Body
email– Pflichtcaptcha– Pflicht, aus GET /api/captcha
Die Antwort ist immer {"ok": true} – auch wenn es kein Konto mit dieser E-Mail gibt (verrät nicht, welche Adressen registriert sind). Der Link führt auf https://notes.secret.vip/?reset=<token>, ist 1 Stunde gültig und einmal nutzbar. Limits: 3 Anfragen pro E-Mail und Stunde, 10 pro IP und Stunde.
Antwort 200
{"ok": true}
Fehler
400 invalid_body 403 captcha_required 429 too_many_attempts 503 mail_unavailable (kein E-Mail-Versand eingerichtet)
curl
curl -s https://notes.secret.vip/api/password/forgot \
-H 'Content-Type: application/json' \
-d '{"email":"ich@example.com","captcha":{"id":"0f3a…","answer":"k7mpx"}}'
POST/api/password/resetöffentlich
Setzt mit dem Token aus der E-Mail ein neues Passwort.
Body
token– Pflicht, derrst_…-Wert aus dem Linkpassword– Pflicht, neues Passwort (Regeln wie bei der Registrierung)
Danach sind alle Sitzungen und alle Geräte-Zugänge (API-Keys) des Kontos widerrufen – Plugins müssen sich neu anmelden. Es wird keine Session gesetzt; anschließend normal anmelden. Ungültige Tokens zählen als Fehlversuch.
Antwort 200
{"ok": true}
Fehler
400 invalid_body 400 weak_password 400 invalid_token (unbekannt, abgelaufen oder schon benutzt) 429 too_many_attempts
curl
curl -s https://notes.secret.vip/api/password/reset \
-H 'Content-Type: application/json' \
-d '{"token":"rst_…","password":"korrekt-pferd-batterie"}'
POST/api/logout
Beendet die Session – oder widerruft den API-Key, mit dem der Aufruf gemacht wurde.
Antwort 200
{"ok": true}
Fehler
401 unauthorized
curl
curl -s -X POST https://notes.secret.vip/api/logout -H 'Authorization: Bearer smn_…'
GET/api/me
Gibt das eigene Konto zurück – praktisch, um einen Token zu prüfen.
Antwort 200
{"user": {"id": 1, "email": "ich@example.com", "name": "Sebastian", "is_admin": false, "email_verified": true, "created_at": "2026-09-01T18:22:04.000Z"}}
Fehler
401 unauthorized
curl
curl -s https://notes.secret.vip/api/me -H 'Authorization: Bearer smn_…'
PATCH/api/me
Ändert den eigenen Anzeigenamen und/oder das Passwort.
Body
name– optional, neuer Anzeigenamepassword– optional, neues Passwort (min. 8 Zeichen)current_password– Pflicht, sobaldpasswordgesetzt ist
Antwort 200
{"user": {"id": 1, "email": "ich@example.com", "name": "Sebastian B.", "is_admin": false, "email_verified": true, "created_at": "2026-09-01T18:22:04.000Z"}}
Fehler
400 invalid_body 400 weak_password 401 unauthorized 401 wrong_credentials (falsches current_password)
curl
curl -s -X PATCH https://notes.secret.vip/api/me \
-H 'Authorization: Bearer smn_…' \
-H 'Content-Type: application/json' \
-d '{"password":"nochGeheimer456","current_password":"geheim123"}'
Notizen
Eine Notiz gehört genau einem Nutzer und hat pro Nutzer einen eindeutigen Namen – den Namen des Ableton-Sets.
Namensregel: 1–200 Zeichen (Unicode), kein /, kein \, keine Steuerzeichen, kein führender Punkt. Sonst 400 invalid_name.
URL-Encoding: Der Name steht im Pfad und wird URL-kodiert. Leerzeichen → %20, Umlaute z. B. ü → %C3%BC. In JavaScript encodeURIComponent(name), in Python urllib.parse.quote(name).
Mein Track → /api/notes/Mein%20Track
Späti Session 2 → /api/notes/Sp%C3%A4ti%20Session%202
permission ist immer eines von "owner" (gehört mir), "write" (geteilt, darf schreiben), "read" (geteilt, nur lesen).
Welche Notiz ist gemeint? – der Query-Parameter owner
Weil mehrere Nutzer eine Notiz mit demselben Namen haben können (z. B. beide arbeiten an „Mein Track“), steuert ?owner=… bei GET, PUT und PATCH auf /api/notes/:name, welche Notiz angesprochen wird:
| Aufruf | Bedeutung |
|---|---|
?owner=me | Nur meine eigene Notiz. Geteilte Notizen werden ignoriert. |
?owner=freund@example.com | Die Notiz dieses Nutzers, die mit mir geteilt ist. Gibt es keine solche Freigabe → 404 not_found. |
ohne owner | Automatische Auflösung: (1) meine eigene Notiz, falls vorhanden; (2) sonst eine mit mir geteilte Notiz dieses Namens – bei mehreren die zuletzt geänderte; (3) sonst „existiert nicht“ – die Notiz gehört dann mir und wird bei PUT/PATCH neu angelegt. |
?owner=me – dann landet nie versehentlich etwas in einer fremden, geteilten Notiz. Die Ableton-Integration lässt owner weg, damit geteilte Sets automatisch gefunden werden.
Bei DELETE wird owner ignoriert – gelöscht werden kann immer nur die eigene Notiz.
GET/api/notes
Listet alle eigenen und alle mit mir geteilten Notizen – neueste zuerst.
Antwort 200
{
"notes": [
{
"name": "Mein Track",
"owner": {"email": "ich@example.com", "name": "Sebastian"},
"mine": true,
"permission": "owner",
"updated_at": "2026-09-03T10:15:30.123Z",
"size": 27
},
{
"name": "Late Night Jam",
"owner": {"email": "freund@example.com", "name": "Alex"},
"mine": false,
"permission": "write",
"updated_at": "2026-09-02T22:41:09.000Z",
"size": 412
}
]
}
size = Größe des Textes in Bytes. mine = ob die Notiz mir gehört.
Fehler
401 unauthorized
curl
curl -s https://notes.secret.vip/api/notes -H 'Authorization: Bearer smn_…'
GET/api/notes/:name
Liest eine Notiz. Existiert sie nicht, kommt trotzdem 200 – mit exists:false und leerem Text.
Query
owner– optional:me, eine E-Mail oder weggelassen (Auflösungsregel)
Header (optional)
If-None-Match– der zuletzt erhalteneETag; unverändert →304ohne Body
Antwort 200
{
"name": "Mein Track",
"text": "Kick lauter\nVocals ab 1:30",
"updated_at": "2026-09-03T10:15:30.123Z",
"exists": true,
"owner": {"email": "ich@example.com", "name": "Sebastian"},
"mine": true,
"permission": "owner"
}
// Notiz existiert nicht
{"name": "Neues Set", "text": "", "updated_at": null, "exists": false,
"owner": {"email": "ich@example.com", "name": "Sebastian"}, "mine": true, "permission": "owner"}
ETag / If-None-Match / 304
Jede 200-Antwort trägt einen Header ETag: "<ms>" – der Änderungszeitpunkt in Millisekunden, in Anführungszeichen. Wer regelmäßig nachfragt (Polling), schickt diesen Wert beim nächsten Mal als If-None-Match mit. Hat sich nichts geändert, antwortet der Server mit 304 Not Modified und leerem Body – spart Traffic und Parsing.
# 1. Lesen – ETag merken
curl -si https://notes.secret.vip/api/notes/Mein%20Track -H 'Authorization: Bearer smn_…'
# HTTP/1.1 200 OK
# ETag: "1788430530123"
# …
# 2. Später nachfragen – nur laden, wenn sich etwas geändert hat
curl -si https://notes.secret.vip/api/notes/Mein%20Track \
-H 'Authorization: Bearer smn_…' \
-H 'If-None-Match: "1788430530123"'
# HTTP/1.1 304 Not Modified (kein Body)
Fehler
400 invalid_name 401 unauthorized 404 not_found (nur bei ?owner=<email> ohne passende Freigabe)
curl
# eigene Notiz
curl -s 'https://notes.secret.vip/api/notes/Mein%20Track?owner=me' -H 'Authorization: Bearer smn_…'
# mit mir geteilte Notiz eines bestimmten Nutzers
curl -s 'https://notes.secret.vip/api/notes/Mein%20Track?owner=freund@example.com' -H 'Authorization: Bearer smn_…'
PUT/api/notes/:name
Ersetzt den gesamten Text einer Notiz; legt sie an, falls sie fehlt. Optional mit Konfliktschutz.
Query
owner– optional:me, eine E-Mail oder weggelassen (Auflösungsregel)
Body (Content-Type muss application/json sein)
text– Pflicht, der komplette neue Text (max. 1 MB)base_updated_at– optional: dasupdated_at, das der Client zuletzt gelesen hat (ISO-String odernull, wenn die Notiz beim Lesen noch nicht existierte). Wenn gesetzt, prüft der Server auf Konflikte.
Antwort 200
{
"name": "Mein Track",
"updated_at": "2026-09-03T10:15:30.123Z",
"owner": {"email": "ich@example.com", "name": "Sebastian"},
"permission": "owner"
}
Konflikt-Verfahren (base_updated_at → 409)
Ohne base_updated_at gewinnt immer der letzte Schreiber – gut für Skripte, riskant für Editoren. Mit base_updated_at läuft es so:
- Client liest die Notiz und merkt sich
updated_at(z. B."2026-09-03T10:15:30.123Z"; beiexists:falseist esnull). - Client schickt beim
PUTgenau diesen Wert alsbase_updated_atmit. - Stimmt er mit dem Stand auf dem Server überein →
200, gespeichert. - Weicht er ab (jemand anderes hat zwischenzeitlich gespeichert) →
409 {"error":"conflict", …}. Der Body enthält den aktuellen Serverstand (text,updated_at). Nichts wurde überschrieben. - Client zeigt beide Fassungen, lässt zusammenführen und schickt erneut – jetzt mit dem
updated_ataus der 409-Antwort alsbase_updated_at.
Sonderfall: base_updated_at ist gesetzt, aber die Notiz existiert auf dem Server nicht (mehr) → ebenfalls 409, mit text:"" und updated_at:null.
# Client hat um 10:15:30.123 gelesen, jemand anderes hat um 10:20:00 gespeichert
curl -s -X PUT https://notes.secret.vip/api/notes/Mein%20Track \
-H 'Authorization: Bearer smn_…' \
-H 'Content-Type: application/json' \
-d '{"text":"Kick lauter\nVocals ab 1:30\nHiHat leiser","base_updated_at":"2026-09-03T10:15:30.123Z"}'
# → HTTP 409
{
"error": "conflict",
"name": "Mein Track",
"text": "Kick lauter\nVocals ab 1:30\nBass ab 0:45 doppeln",
"updated_at": "2026-09-03T10:20:00.000Z",
"owner": {"email": "ich@example.com", "name": "Sebastian"},
"permission": "owner"
}
# Zusammenführen und erneut senden – mit dem neuen Stand als Basis
curl -s -X PUT https://notes.secret.vip/api/notes/Mein%20Track \
-H 'Authorization: Bearer smn_…' \
-H 'Content-Type: application/json' \
-d '{"text":"Kick lauter\nVocals ab 1:30\nBass ab 0:45 doppeln\nHiHat leiser","base_updated_at":"2026-09-03T10:20:00.000Z"}'
# → HTTP 200
Fehler
400 invalid_name 400 invalid_json 400 invalid_body 401 unauthorized 403 forbidden (Freigabe ist nur-lesen) 403 quota_exceeded (Anlegen: schon 1000 Notizen) 404 not_found (?owner=<email> ohne Freigabe) 409 conflict 413 payload_too_large (> 1 MB) 415 unsupported_media_type (Content-Type fehlt/falsch)
curl
curl -s -X PUT 'https://notes.secret.vip/api/notes/Mein%20Track?owner=me' \
-H 'Authorization: Bearer smn_…' \
-H 'Content-Type: application/json' \
-d '{"text":"Kick lauter\nVocals ab 1:30"}'
PATCH/api/notes/:name
Hängt Text als eigene Zeile an die Notiz an; legt sie an, falls sie fehlt. Gemacht für Automationen – kein Konfliktcheck nötig.
Query
owner– optional:me, eine E-Mail oder weggelassen (Auflösungsregel)
Body (Content-Type application/json)
append– Pflicht, der anzuhängende Text. Wird als eigene Zeile ergänzt.
Antwort 200 – mit dem vollständigen neuen Text
{
"name": "Mein Track",
"updated_at": "2026-09-03T10:25:12.480Z",
"text": "Kick lauter\nVocals ab 1:30\nMaster -1 dB",
"owner": {"email": "ich@example.com", "name": "Sebastian"},
"permission": "owner"
}
Fehler
400 invalid_name 400 invalid_json 400 invalid_body 401 unauthorized 403 forbidden (nur-lesen) 404 not_found 413 payload_too_large 415 unsupported_media_type
curl
curl -s -X PATCH https://notes.secret.vip/api/notes/Mein%20Track \
-H 'Authorization: Bearer smn_…' \
-H 'Content-Type: application/json' \
-d '{"append":"Master -1 dB"}'
DELETE/api/notes/:name
Löscht die eigene Notiz samt aller Freigaben. Antwortet auch dann mit ok, wenn sie gar nicht existiert.
Nur der Besitzer darf löschen. ?owner wird ignoriert. Eine mit mir geteilte (fremde) Notiz kann ich nicht löschen → 403 forbidden.
Antwort 200
{"ok": true}
Fehler
400 invalid_name 401 unauthorized 403 forbidden
curl
curl -s -X DELETE https://notes.secret.vip/api/notes/Mein%20Track -H 'Authorization: Bearer smn_…'
Teilen nur Besitzer
Freigaben verwaltet ausschließlich der Besitzer einer Notiz. permission ist "read" (nur lesen) oder "write" (lesen + schreiben). Wer keine Freigabe hat, darf die Notiz nicht sehen.
API-Keys eigene
Jeder Key ist ein Bearer-Token smn_…. Ein Key pro Gerät oder Automation ist sinnvoll – dann lässt sich jeder einzeln widerrufen.
GET/api/keys
Listet die eigenen API-Keys (ohne den geheimen Token – nur der Anfang als prefix).
Antwort 200
{
"keys": [
{"id": 3, "name": "Ableton Live (MacBook)", "prefix": "smn_9f2c", "created_at": "2026-09-01T18:30:00.000Z", "last_used_at": "2026-09-03T10:25:12.000Z", "current": true},
{"id": 4, "name": "n8n", "prefix": "smn_0b7e", "created_at": "2026-09-02T09:00:00.000Z", "last_used_at": null, "current": false}
]
}
current = der Key, mit dem dieser Aufruf gemacht wurde. last_used_at ist null, wenn der Key noch nie benutzt wurde.
Fehler
401 unauthorized
curl
curl -s https://notes.secret.vip/api/keys -H 'Authorization: Bearer smn_…'
POST/api/keys
Erzeugt einen neuen API-Key. Der Token wird nur dieses eine Mal gezeigt.
Body
name– Pflicht, z. B. Gerät oder Zweck
Antwort 200
{"id": 5, "name": "n8n Studio", "token": "smn_…"}
Fehler
400 invalid_body 401 unauthorized 403 quota_exceeded (schon 50 Geräte-Zugänge)
curl
curl -s https://notes.secret.vip/api/keys \
-H 'Authorization: Bearer smn_…' \
-H 'Content-Type: application/json' \
-d '{"name":"n8n Studio"}'
DELETE/api/keys/:id
Widerruft einen API-Key sofort.
Antwort 200
{"ok": true}
Fehler
401 unauthorized 404 not_found
curl
curl -s -X DELETE https://notes.secret.vip/api/keys/5 -H 'Authorization: Bearer smn_…'
Admin nur is_admin
Alle Admin-Routen erfordern ein Konto mit is_admin:true – sonst 403 forbidden. Der erste registrierte Nutzer ist automatisch Admin.
GET/api/admin/users
Listet alle Nutzer mit Anzahl ihrer Notizen.
Antwort 200
{
"users": [
{"id": 1, "email": "ich@example.com", "name": "Sebastian", "is_admin": true, "email_verified": true, "created_at": "2026-09-01T18:22:04.000Z", "notes": 12},
{"id": 2, "email": "freund@example.com", "name": "Alex", "is_admin": false, "email_verified": true, "created_at": "2026-09-02T19:40:00.000Z", "notes": 3}
]
}
Fehler
401 unauthorized 403 forbidden
curl
curl -s https://notes.secret.vip/api/admin/users -H 'Authorization: Bearer smn_…'
POST/api/admin/invites
Erzeugt einen Einladungslink – 7 Tage gültig, einmal nutzbar.
Body
email– optional; wenn gesetzt, ist die Einladung an diese Adresse gebunden
Antwort 200
{
"id": 7,
"url": "https://notes.secret.vip/?invite=a1b2c3…",
"email": "neu@example.com",
"expires_at": "2026-09-10T10:30:00.000Z"
}
Fehler
400 invalid_body 401 unauthorized 403 forbidden
curl
curl -s https://notes.secret.vip/api/admin/invites \
-H 'Authorization: Bearer smn_…' \
-H 'Content-Type: application/json' \
-d '{"email":"neu@example.com"}'
GET/api/admin/invites
Listet alle Einladungen – offene, abgelaufene und benutzte.
Antwort 200
{
"invites": [
{"id": 7, "email": "neu@example.com", "created_at": "2026-09-03T10:30:00.000Z", "expires_at": "2026-09-10T10:30:00.000Z", "used_at": null},
{"id": 6, "email": null, "created_at": "2026-08-20T12:00:00.000Z", "expires_at": "2026-08-27T12:00:00.000Z", "used_at": "2026-08-21T09:15:00.000Z"}
]
}
Fehler
401 unauthorized 403 forbidden
curl
curl -s https://notes.secret.vip/api/admin/invites -H 'Authorization: Bearer smn_…'
DELETE/api/admin/invites/:id
Löscht eine Einladung; der Link funktioniert danach nicht mehr.
Antwort 200
{"ok": true}
Fehler
401 unauthorized 403 forbidden 404 not_found
curl
curl -s -X DELETE https://notes.secret.vip/api/admin/invites/7 -H 'Authorization: Bearer smn_…'
POST/api/admin/users/:id/password
Setzt einem Nutzer ein neues Passwort. Alle seine Sessions enden.
Body
password– Pflicht, min. 8 Zeichen
Antwort 200
{"ok": true}
Fehler
400 invalid_body 400 weak_password 401 unauthorized 403 forbidden 404 user_not_found
curl
curl -s https://notes.secret.vip/api/admin/users/2/password \
-H 'Authorization: Bearer smn_…' \
-H 'Content-Type: application/json' \
-d '{"password":"neuesPasswort789"}'
PATCH/api/admin/users/:id
Ändert Name und/oder Admin-Status eines Nutzers.
Body
is_admin– optional,true/falsename– optional, neuer Anzeigename
Antwort 200
{"user": {"id": 2, "email": "freund@example.com", "name": "Alex", "is_admin": true, "email_verified": true, "created_at": "2026-09-02T19:40:00.000Z"}}
Fehler
400 invalid_body 401 unauthorized 403 forbidden 404 user_not_found
curl
curl -s -X PATCH https://notes.secret.vip/api/admin/users/2 \
-H 'Authorization: Bearer smn_…' \
-H 'Content-Type: application/json' \
-d '{"is_admin":true}'
DELETE/api/admin/users/:id
Löscht einen Nutzer komplett – samt Notizen, API-Keys und Freigaben. Nicht rückgängig zu machen.
Antwort 200
{"ok": true}
Fehler
400 cannot_delete_self (eigenes Konto) 401 unauthorized 403 forbidden 404 user_not_found
curl
curl -s -X DELETE https://notes.secret.vip/api/admin/users/2 -H 'Authorization: Bearer smn_…'
Sonstiges
GET/api/healthöffentlich
Lebenszeichen des Servers – für Monitoring.
Antwort 200
{"ok": true}
curl
curl -s https://notes.secret.vip/api/health
GET/api/docsöffentlich
Diese Dokumentation als HTML.
curl
curl -s https://notes.secret.vip/api/docs
GET/api/openapi.jsonöffentlich
Maschinenlesbare Beschreibung der API (OpenAPI 3.0) – z. B. für Postman, Insomnia oder Code-Generatoren.
curl
curl -s https://notes.secret.vip/api/openapi.json
n8n / Automationen
Für Automationen ist der Bearer-Token der richtige Weg. Am besten pro Automation einen eigenen API-Key anlegen.
n8n: HTTP-Request-Node einrichten
- Node HTTP Request hinzufügen.
- Authentication → Generic Credential Type → Header Auth.
- Neue Credential anlegen: Name =
Authorization, Value =Bearer smn_…(dein Token). - Bei
PUT/PATCH/POST: Send Body aktivieren, Body Content Type JSON. n8n setzt dannContent-Type: application/json. Falls du den Header manuell setzt: Send Headers →Content-Type=application/json. - URL mit kodiertem Notiznamen, z. B.
https://notes.secret.vip/api/notes/Mein%20Track. Bei dynamischen Namen:{{ encodeURIComponent($json.setName) }}.
Typische Anwendungsfälle
| Ziel | Node-Einstellung | Body (JSON) |
|---|---|---|
| Zeile an eine Notiz anhängen (z. B. „Export fertig 14:32“, Kalender-Eintrag, Reminder) |
PATCH /api/notes/Mein%20Track |
{"append":"Export fertig 14:32"} |
| Alle Notizen auflisten (z. B. Tages-Digest, Backup, Suche) |
GET /api/notes |
– |
PATCH ist für Automationen ideal: keine Konflikte, kein Vorher-Lesen, legt die Notiz bei Bedarf an – und gibt den vollständigen neuen text zurück.
JavaScript (fetch)
const token = "smn_…";
const name = "Mein Track";
const res = await fetch("https://notes.secret.vip/api/notes/" + encodeURIComponent(name), {
method: "PATCH",
headers: { "Authorization": "Bearer " + token, "Content-Type": "application/json" },
body: JSON.stringify({ append: "Master -1 dB" })
});
if (!res.ok) throw new Error("SMN " + res.status + ": " + (await res.json()).error);
console.log((await res.json()).text);
Python (requests)
import requests
from urllib.parse import quote
TOKEN = "smn_…"
headers = {"Authorization": f"Bearer {TOKEN}"}
url = "https://notes.secret.vip/api/notes/" + quote("Mein Track")
r = requests.patch(url, headers=headers, json={"append": "Master -1 dB"})
r.raise_for_status() # wirft bei 4xx/5xx; Details in r.json()["error"]
print(r.json()["text"])
json= setzt in requests automatisch Content-Type: application/json.
Fehlercodes
Fehler kommen immer als {"error":"<code>"}. Ausnahme: 409 conflict enthält zusätzlich den Serverstand der Notiz (siehe PUT).
| HTTP | error | Bedeutung |
|---|---|---|
| 400 | invalid_name | Notizname verletzt die Namensregel (Länge, /, \, Steuerzeichen, führender Punkt). |
| 400 | invalid_body | Pflichtfeld fehlt oder hat den falschen Typ. |
| 400 | invalid_json | Body ist kein gültiges JSON. |
| 400 | weak_password | Passwort zu schwach: unter 8 Zeichen, Allerweltspasswort, nur ein wiederholtes Zeichen oder gleich der E-Mail. |
| 400 | cannot_share_self | Eine Notiz kann nicht mit sich selbst geteilt werden. |
| 400 | cannot_delete_self | Admin kann das eigene Konto nicht löschen. |
| 400 | registration_closed | Registrierung ist geschlossen – Einladung nötig. |
| 400 | invalid_invite | Einladungs-Token unbekannt, abgelaufen oder schon benutzt. |
| 400 | invalid_token | Passwort-Reset-Token unbekannt, abgelaufen oder schon benutzt. |
| 401 | unauthorized | Kein oder ungültiger Token bzw. keine gültige Session. |
| 401 | wrong_credentials | E-Mail/Passwort falsch – oder current_password stimmt nicht. |
| 403 | forbidden | Keine Berechtigung: nur-lesen-Freigabe, fremde Notiz, kein Admin. |
| 403 | captcha_required | Sicherheitscode nötig, fehlt, falsch oder abgelaufen – neues Bild über GET /api/captcha holen. |
| 403 | email_unverified | Login: Passwort stimmt, aber die E-Mail-Adresse ist noch nicht bestätigt – Link aus der Bestätigungs-Mail öffnen oder POST /api/register/resend. |
| 403 | quota_exceeded | Kontingent erreicht (1000 Notizen pro Konto, 50 Freigaben pro Notiz, 50 Geräte-Zugänge). Body enthält zusätzlich limit und what. |
| 404 | not_found | Notiz, Freigabe, Key oder Einladung nicht gefunden. |
| 404 | user_not_found | Kein Nutzer mit dieser E-Mail bzw. ID. |
| 409 | conflict | Notiz wurde zwischenzeitlich geändert – Body enthält den Serverstand. |
| 409 | email_taken | E-Mail ist bereits registriert. |
| 413 | payload_too_large | Notiz größer als 1 MB. |
| 415 | unsupported_media_type | Content-Type ist nicht application/json. |
| 429 | too_many_attempts | Rate-Limit: 10 Login-Fehlversuche/15 min/IP, 5 Registrierungen/h/IP, 3 Passwort-Anfragen/h/E-Mail (10/h/IP), 30 Captchas/10 min/IP – kurz warten. |
| 500 | server_error | Unerwarteter Serverfehler. |
| 503 | mail_unavailable | E-Mail-Versand auf diesem Server nicht eingerichtet – Passwort vergessen und Bestätigungs-Mail erneut senden nicht verfügbar. |
Limits
- 1 MB pro Notiz – größere Texte werden mit
413 payload_too_largeabgelehnt. - Login: nach 3 Fehlversuchen (pro IP oder pro E-Mail) ist ein Captcha Pflicht (
403 captcha_required); nach 10 Fehlversuchen in 15 Minuten pro IP429 too_many_attempts. - Registrierung: Captcha (außer erstes Konto oder Einladung), 5 Konten pro IP und Stunde, Honeypot-Feld
website. - E-Mail-Bestätigung: Link 24 Stunden gültig, einmal nutzbar; erneut senden 3× pro E-Mail und Stunde, 10× pro IP und Stunde; nie bestätigte Konten werden nach 7 Tagen gelöscht.
- Passwort vergessen: Captcha, 3 Anfragen pro E-Mail und Stunde, 10 pro IP und Stunde; Link 1 Stunde gültig, einmal nutzbar; das Zurücksetzen widerruft alle Sitzungen und Geräte-Zugänge.
- Captcha: 30 Bilder pro IP und 10 Minuten; jedes 10 Minuten gültig und genau einmal prüfbar.
- Kontingente: 1000 Notizen pro Konto, 50 Freigaben pro Notiz, 50 Geräte-Zugänge pro Konto – darüber
403 quota_exceeded. - Namensregel: 1–200 Zeichen (Unicode), kein
/, kein\, keine Steuerzeichen, kein führender Punkt → sonst400 invalid_name. - Passwort: mindestens 8 Zeichen, kein Allerweltspasswort (Blockliste), nicht nur ein wiederholtes Zeichen, nicht gleich der E-Mail.
- Session-Cookie: 30 Tage gültig. Einladungslinks: 7 Tage, einmal nutzbar.