nessie

API

Die HTTP-API hinter jedem Nessie-Client und wie Sie sich an ihr authentifizieren.

Was die API ist

Nessie hat eine einzige HTTP-API. Die Admin-Webanwendung, die Clients für macOS und iOS und der Remote-Executor-Daemon sind gewöhnliche Clients dieser API, und nichts, was sie tun können, ist auf anderem Weg erreichbar. Es handelt sich um eine JSON-API auf Basis eines Fastify-Servers. Sie ist kein GraphQL und hat keine gesonderte „öffentliche“ Teilmenge: Die Oberfläche, die Ihre eigenen Skripte aufrufen, ist dieselbe, die das Produkt selbst aufruft.

Der Pfad enthält kein Versionssegment. Alle Routen liegen unter /api, mit Ausnahme des eingehenden MCP-Endpunkts POST /mcp und einiger /.well-known/-Discovery-Dokumente. Die Kompatibilität wird gewahrt, indem nur Felder hinzugefügt werden. Ein Client, der unbekannte Felder ignoriert, funktioniert deshalb weiter.

Basis-URL

Nessie wird selbst gehostet, daher ist die Basis-URL der Ursprung, unter dem Ihr Deployment die API bereitstellt. Der Betreiber legt sie als NESSIE_API_PUBLIC_URL fest, und derselbe Wert dient der API, wenn sie eine eigene absolute URL erzeugen muss. Auf der Instanz, die wir betreiben, lautet er https://api.nessie.works; eine Entwicklungsinstanz, die mit pnpm dev gestartet wird, antwortet unter http://localhost:5454.

Die Admin-Webanwendung hat einen anderen Ursprung als die API, daher unterliegen Browser-Clients einer CORS-Freigabeliste (NESSIE_CORS_ORIGINS). Anfragen von einem Skript oder einem Server haben keinen Ursprung, der geprüft werden könnte, und sind davon nicht betroffen.

Authentifizierung

Jede Anfrage außerhalb einer kleinen Gruppe bewusst öffentlicher Routen trägt ein Zugriffstoken im Standard-Header:

Authorization: Bearer <token>

Das Token ist ein signiertes JWT, das die API ausstellt. Es ist kurzlebig – standardmäßig dreißig Minuten (NESSIE_AUTH_TOKEN_TTL) – und ist die einzige Anmeldeinformation, die ein gewöhnlicher Aufrufer jemals vorlegt. Nessie hat keine langlebigen persönlichen API-Schlüssel. Wenn ein Skript die API aufrufen soll, meldet es sich genauso an wie der Browser einer Person und erneuert sein Token während des Betriebs.

Token abrufen

POST /api/auth/session legt eine Sitzung an. Die Antwort enthält das Zugriffstoken und die Identität des Aufrufers und setzt das Erneuerungstoken als eigenes HTTP-only-Cookie.

Wie Sie sich ausweisen, hängt davon ab, wie die Instanz bereitgestellt ist. Eine Instanz im Modus local akzeptiert E-Mail-Adresse und Passwort. Auf einer Instanz im Modus hosted oder selfHosted wird die Anmeldung mit Passwort ausnahmslos mit 403 PASSWORD_AUTH_DISABLED abgelehnt, und der einzige Zugang ist der konfigurierte Identitätsanbieter. So kann das Konto nie einen zweiten, schwächeren Identitätsweg neben ihm bekommen.

Anmeldung über einen Identitätsanbieter

Single Sign-on ist ein OAuth-Autorisierungscode-Austausch mit PKCE in drei Schritten. Der eigene Identitätsanbieter von Nessie ist UnlikeOtherAI, der auch die Organisations- und Teamstruktur verwaltet, die eine Instanz abbildet; der folgende Austausch hat für jeden konfigurierten Anbieter dieselbe Form.

  1. GET /api/auth/providers listet alle Anbieter auf, die diese Instanz akzeptiert, jeweils mit einer providerId, einer Bezeichnung und der Angabe, ob die Anmeldeseite automatisch dorthin weiterleiten soll.
  2. GET /api/auth/providers/:providerId/authorize antwortet mit den Query-Parametern codeChallenge, redirectUri und state mit { data: { authorizeUrl } }. Leiten Sie die Person dorthin weiter.
  3. Der Anbieter schickt die Person mit einem Code an Ihre redirectUri zurück. Senden Sie diesen Code als POST /api/auth/session mit { providerId, code, codeVerifier, redirectUri } zurück. Alle vier Angaben sind gemeinsam erforderlich; eine unvollständige Angabe ergibt 400 EXTERNAL_AUTH_INCOMPLETE.

Sitzung erneuern

Das Erneuerungstoken steht nie im Antwortkörper. Es wird als HTTP-only-Cookie mit dem Namen nessie_refresh gesetzt, auf den Pfad /api/auth beschränkt und hat standardmäßig eine Gültigkeit von dreißig Tagen (NESSIE_AUTH_REFRESH_TOKEN_TTL). Ein Aufruf von POST /api/auth/refresh mit diesem Cookie verbraucht es, stellt ein neues Zugriffstoken aus und setzt an dessen Stelle ein neues Cookie. Die Tokens rotieren, daher ist ein gestohlenes nur bis zur nächsten Erneuerung durch den echten Client gültig.

Ein Browser, der bei mehreren Konten angemeldet ist, behält pro Anmeldung ein solches Cookie, benannt nessie_refresh_<slot>, und gibt mit dem Header x-nessie-account-slot an, auf welches Cookie sich eine Anfrage bezieht. Ein Client, der keinen solchen Header sendet, meint nessie_refresh; das genügt einem Kommandozeilen-Client.

Ein Kommandozeilen-Client braucht daher einen Cookie-Jar. Mit curl ist das --cookie-jar beim Anmeldeaufruf und --cookie bei der Erneuerung, wie im Beispiel unten.

Was bei jeder Anfrage geprüft wird

Authentifizierung besteht nicht nur aus einer Signaturprüfung. Bevor ein Handler läuft, prüft die API Signatur und Ablaufdatum des Tokens, bestätigt, dass die Sitzung nicht widerrufen wurde (sowohl über die benutzerweite Token-Version als auch über die einzelne Sitzungs-ID), und lädt die Mitgliedschaft des Aufrufers in der Organisation, die das Token nennt. Eine deaktivierte Mitgliedschaft ergibt ein 403. Ist die Organisation an einen externen Identitätsanbieter angebunden, wird der aktuelle Status des Aufrufers bei jeder Anfrage beim Anbieter neu geprüft, statt ihn aus dem Token zu lesen. So wirkt ein dort entzogener Zugriff sofort und nicht erst beim nächsten Ablauf.

Anmeldeinformationen, die keine Sitzungstokens sind

Es gibt zwei weitere Bearer-Credentials, und keines davon ist ein allgemeiner Schlüssel. Ein Sprachgeräte-Credential wird nur auf den Sprachanruf-Routen akzeptiert; ein Agent-Zugangs-Credential nur auf dem MCP-Endpunkt. Wer eines davon anderswo vorlegt, wird mit einem 403 abgelehnt, das das Credential nennt, und nicht stillschweigend als schwächere Sitzung behandelt. Siehe MCP und verbundene Tools, wie das Agent-Credential ausgestellt wird.

Das Antwortformat

Jede erfolgreiche Antwort ist ein Objekt mit dem Schlüssel data. Es gibt kein nacktes Array und keine Nutzdaten auf oberster Ebene, sodass ein Client nie anhand der Form unterscheiden muss:

{
  "data": { "id": "3f1c…", "label": "engineering" }
}

Listenendpunkte fügen neben den Daten ein meta-Objekt hinzu. Auf oberster Ebene wird nie etwas anderes ergänzt.

Fehler haben unabhängig von ihrer Ursache ebenfalls ein einheitliches Format:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "A message needs content or at least one attachment",
    "field": "body",
    "details": { "formErrors": [], "fieldErrors": {} }
  }
}

code ist der maschinenlesbare Teil, und danach sollten Sie Ihre Logik ausrichten; message ist für Menschen bestimmt. field und details sind vorhanden, wenn der Fehler genau genug ist, um ihn zu lokalisieren. Ein unbehandelter Serverfehler ist immer 500 INTERNAL_ERROR mit einer festen Meldung. Der eigentliche Fehlertext wird protokolliert, nie zurückgegeben, weil er auf dem Weg nach draußen Anfragedaten aufgenommen haben könnte.

Die Fehlercodes, die Ihnen zuerst begegnen

StatusCodeBedeutung
400VALIDATION_ERRORDer Körper oder die Abfrage entsprach nicht dem Schema.
401AUTH_REQUIREDKein Bearer-Token vorhanden oder der Header war fehlerhaft.
401TOKEN_EXPIREDDas Zugriffstoken ist abgelaufen. Erneuern Sie es und wiederholen Sie die Anfrage.
401TOKEN_REVOKEDDie Sitzung wurde abgemeldet oder alle Sitzungen wurden beendet.
403ACCOUNT_DEACTIVATEDDie Mitgliedschaft in dieser Organisation wurde deaktiviert.
404NOT_FOUNDKeine solche Route oder kein für diesen Aufrufer sichtbarer Datensatz.
429RATE_LIMITEDEin Kontingent ist ausgeschöpft. Beachten Sie Retry-After.
500INTERNAL_ERROREin unbehandelter Serverfehler, bewusst undurchsichtig gehalten.

Paginierung

Listenendpunkte blättern mit einem undurchsichtigen Keyset-Cursor, nicht mit einem Offset. Datensätze werden eingefügt, während jemand liest, und bei einem Offset wiederholt die zweite Seite stillschweigend einen Datensatz, den die erste Seite schon gezeigt hat.

Drei Query-Parameter werden akzeptiert: cursor, limit und direction (forward oder backward). Die Standardseite umfasst 25 Einträge, die Obergrenze liegt bei 100; ein größerer Wert wird auf die Obergrenze gesetzt statt abgelehnt. Endpunkte, die zusätzlich sortieren oder filtern, ergänzen sort, order und eine Freitextsuche q.

Das meta-Objekt der Seite zeigt Ihnen, wo Sie stehen:

{
  "data": [ … ],
  "meta": {
    "hasMore": true,
    "nextCursor": "2026-09-12T08:31:44.005Z|8b2f…",
    "prevCursor": null,
    "total": 134
  }
}

nextCursor und prevCursor sind beide vorhanden: Zurück und Weiter sind Schaltflächen, die eine Person erwartet. Die Cursor sind undurchsichtig. Der Server kodiert seinen eigenen Sortierschlüssel darin, und ein Client, der einen davon parst oder selbst baut, bricht, sobald eine Liste ihre Sortierung ändert. total ist bei den meisten Listen vorhanden und fehlt dort, wo Zählen nicht sinnvoll ist, etwa bei Ranglisten-Suchergebnissen.

Rate-Limits

Limits gelten pro festem Zeitfenster und werden durchgesetzt, bevor der Anfragekörper überhaupt gelesen wird. Eine abgelehnte Anfrage ergibt 429 mit dem Code RATE_LIMITED und einem Header Retry-After in Sekunden.

Die meisten Kontingente sind an die Client-IP gebunden. Die für Anmeldedaten sensiblen – Anmeldung, Erneuerung, erneute Passwortbestätigung und Schreibzugriffe auf MCP-Credentials – führen zusätzlich einen Zähler pro Konto und lehnen ab, wenn einer der beiden Zähler auslöst. So stoßen sowohl eine Adresse, die viele Konten angreift, als auch ein Konto, das von vielen Adressen aus angegriffen wird, auf eine Grenze. Die Anmeldung erlaubt standardmäßig 10 Versuche pro IP und 5 pro E-Mail-Adresse innerhalb von zehn Minuten; das Senden einer Nachricht standardmäßig 60 pro Minute; Schreibvorgänge unter /api/agents 60 pro Minute. Jeder Schwellenwert ist pro Installation konfigurierbar, betrachten Sie diese Werte daher als ausgelieferte Standardwerte und nicht als Vertrag. Health-Checks und die Echtzeit-Streams sind ausgenommen.

Echtzeit

Eine Liste per Polling abzufragen ist der falsche Weg, ein Gespräch zu verfolgen. Dafür gibt es drei Streaming-Endpunkte:

  • GET /api/events/stream – Server-Sent Events für alles, was die angemeldete Person sehen kann.
  • GET /api/threads/:threadId/stream – Server-Sent Events für einen Thread. Er berücksichtigt Last-Event-ID, sodass eine Wiederverbindung fortsetzt, statt neu zu starten.
  • GET /api/activity – ein WebSocket, der den Aktivitätsfeed überträgt.

Browser können bei einem WebSocket-Handshake keine Header setzen, daher akzeptiert die WebSocket-Route das Zugriffstoken zusätzlich als Query-Parameter ?token=. Das ist der einzige Ort, an dem ein Token in einer URL steht; jede andere Route nimmt den Header.

Die Ressourcenfamilien

Es gibt weit über tausend Endpunkte – viel zu viele, um sie alle aufzulisten. Sie sind in Familien gruppiert; wenn Sie das Präfix einer Familie kennen, sind die einzelnen Routen vorhersehbar: eine Sammlung unter dem Präfix, ein Datensatz darunter und benannte Aktionen als POST-Unterpfade.

FamiliePräfixWas sie abdeckt
Authentifizierung/api/authAnmeldung, Erneuerung, Abmeldung, die aktuelle Identität und die eigenen Sitzungen einer Person.
Kanäle und Threads/api/channels, /api/threadsDas Gesprächsmodell: Kanäle enthalten Threads, Threads enthalten Nachrichten, Antworten sind unter einer Wurzelnachricht verschachtelt.
Agenten/api/agentsAgentendefinitionen, deren Kanalbindungen, Gespräche, Aufgaben und Status.
Projekte und Boards/api/projects, /api/tasks, /api/iterationsAufgabenverwaltung: Boards und Spalten unter einem Projekt, Aufgaben darauf und Iterationen darüber.
Wissen/api/knowledge-baseBereiche mit versionierten Seiten und Dateien, mit Kommentaren, Rückverweisen und Suche.
E-Mail/api/mail, /api/mailbox-connectionsVerbundene Postfächer, Agenten-Postfächer und Entwürfe, die ein Agent für eine Person zur Freigabe geschrieben hat.
Freigaben/api/approvalsDie Warteschlange der Aktionen, die ein Agent nicht eigenmächtig ausführen darf, und der eine Aufruf, der sie entscheidet.
Audit/api/audit-logDer hashverkettete Audit-Trail der Organisation mit einem Verifikationsendpunkt. Eigentümer und Admins.
Kosten und Nutzung/api/ledger, /api/billingToken- und Kostenabrechnung, Budgets und Laufzeiten; getrennt davon Guthaben und Aufladungen. Eigentümer und Admins.
Organisation und Teams/api/organizations, /api/teams, /api/usersVerwaltung von Mandant, Teams und Mitgliedschaften, einschließlich Einladungen.
Trigger/api/triggersZeitpläne und Webhooks, die Agentenarbeit starten, mit ihrem Auslöseverlauf.
Anrufe und Sprache/api/calls, /api/voiceAnrufe im Kanal sowie Sitzung, Transkript und Gerätetokens des Sprachassistenten.
Executors/api/executors, /api/execution-environment-templatesDie Rechner, auf denen Agenten arbeiten, ihre Umgebungen und der Kanal für die Kopplung des Daemons.
Läufe/api/runsLaufende Agentenläufe: abbrechen, fortsetzen, neu starten, an einen Executor binden.
Uploads/api/uploads, /api/attachmentsZuerst eine Datei hochladen, dann die zurückgegebene ID an eine Nachricht binden.
Suche/api/messages/searchNachrichtensuche, organisationsweit oder auf einen Kanal beschränkt.
Secrets und Einstellungen/api/secrets, /api/settings/scopedSchreibgeschützte Speicherung von Zugangsdaten mit ausdrücklichen Freigaben sowie geschichtete Konfigurationswerte.
Verbundene Tools/api/mcpMCP-Server, die diese Instanz selbst aufruft. Siehe MCP und verbundene Tools.
Health-Checks/api/healthLebendigkeits- und Bereitschaftsprüfung, beide öffentlich. /api/health/ready prüft zusätzlich die Datenbank.

Berechtigungen werden pro Route durchgesetzt, nicht pro Familie. Grob gesagt sind das Audit-Protokoll, das Kostenhauptbuch, die Richtlinienregeln und die Steuerungsebene der Inferenz Oberflächen für Organisationseigentümer und Admins, während Gespräche, Projekte, Wissen und Freigaben jedem Mitglied offenstehen und auf das eingeschränkt sind, was dieses Mitglied tatsächlich sehen kann.

Beispiele

Anmelden und das Erneuerungs-Cookie behalten

Auf einer Instanz im Modus local, mit Passwort. Das Zugriffstoken kommt im Antwortkörper zurück; das Erneuerungstoken landet im Cookie-Jar.

curl -sS https://api.example.com/api/auth/session \
  --cookie-jar nessie.cookies \
  -H 'Content-Type: application/json' \
  -d '{"email":"ada@example.com","password":"…"}'
{
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
    "me": {
      "user": {
        "id": "1d4e…",
        "email": "ada@example.com",
        "displayName": "Ada Lovelace",
        "roleIds": ["owner"],
        "superAdmin": false
      },
      "session": {
        "sessionId": "9c07…",
        "issuedAt": "2026-09-16T09:00:00.000Z",
        "expiresAt": "2026-09-16T09:30:00.000Z"
      },
      "context": {
        "organizationId": "5a81…",
        "projectId": "7b12…",
        "teamId": "2e60…",
        "channelId": null,
        "bootstrapMode": false
      },
      "auth": {
        "providerId": "local",
        "providerType": "local-bootstrap",
        "autoRedirectToSso": false
      }
    }
  }
}

Wenn das Token abläuft, tauschen Sie das Cookie gegen ein neues:

curl -sS -X POST https://api.example.com/api/auth/refresh \
  --cookie nessie.cookies --cookie-jar nessie.cookies

Die Antwort hat dieselbe { token, me }-Form, und der Cookie-Jar enthält jetzt das rotierte Erneuerungstoken.

Die Kanäle auflisten, die Sie sehen

curl -sS https://api.example.com/api/channels \
  -H "Authorization: Bearer $TOKEN"
{
  "data": [
    {
      "id": "8b2f…",
      "label": "engineering",
      "type": "standard",
      "visibility": "public",
      "projectId": "…",
      "projectName": "Platform",
      "teamId": "…",
      "defaultThreadId": "c41a…",
      "unreadCount": 3,
      "lastMessageAt": "2026-09-15T18:22:07.441Z",
      "viewerCanManage": true
    }
  ]
}

Hängen Sie ?teamId=… an, um auf ein Team einzugrenzen, oder ?includeArchived=true, um archivierte Kanäle einzuschließen. Die defaultThreadId ist der Thread, an den Sie als Nächstes senden.

Eine Nachricht senden

Eine Nachricht braucht entweder etwas content oder mindestens einen hochgeladenen Anhang. Senden Sie einen Idempotency-Key, dann wird eine wiederholte Anfrage der Nachricht zugeordnet, die der erste Versuch angelegt hat, statt eine zweite Kopie zu senden.

curl -sS https://api.example.com/api/threads/c41a…/messages \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: 0f9c2d16-8f4a-4c1e-9a77-6b0d4f2e1a53' \
  -d '{"content":"Deploying the migration now."}'

Eine neue Nachricht ergibt 201:

{
  "data": {
    "message": {
      "id": "d70e…",
      "threadId": "c41a…",
      "role": "user",
      "content": "Deploying the migration now.",
      "createdAt": "2026-09-16T09:04:11.882Z"
    },
    "pendingAgentInvites": []
  }
}

Eine Wiederholung mit demselben Idempotency-Key antwortet mit 200 und der ursprünglichen Nachricht sowie einem leeren pendingAgentInvites, weil der erste Versuch die Benachrichtigung und den Versand bereits erledigt hat, die dieses Array beschreibt.

Zwei Ablehnungen dieser Route sollten Sie gezielt behandeln. Inhalt über der Zeichenlänge eines Chats ergibt 413 MESSAGE_TOO_LARGE; die richtige Reaktion ist, den Text als Datei hochzuladen. Inhalt, der strukturell wie ein Zugangsdatum aussieht, ergibt 422 SECRET_INTERCEPTED. Der Server lehnt ab, bevor die Nachricht gespeichert, zugestellt oder protokolliert wird, und das Zugangsdatum gehört in /api/secrets.