nessie

API

HTTP-API'et bag hver Nessie-klient, og hvordan du godkender dig over for det.

Hvad API'et er

Nessie har ét HTTP-API. Admin-webappen, macOS- og iOS-klienterne og executor-dæmonen er alle almindelige klienter af det, og intet, de kan gøre, kan nås på nogen anden måde. Det er et JSON-API oven på en Fastify-server, det er ikke GraphQL, og der findes ingen separat »offentlig« delmængde: den flade, dine egne scripts kalder, er den samme flade, produktet kalder.

Der er ingen versionsdel i stien. Alle ruter ligger under /api, undtagen det indgående MCP-endpoint på POST /mcp og et par /.well-known/-discoverydokumenter. Kompatibilitet sikres ved kun at tilføje felter, så en klient, der ignorerer det, den ikke kender, fortsat virker.

Basis-URL

Nessie hostes selv, så basis-URL'en er den adresse, din installation serverer API'et på. Operatøren sætter den som NESSIE_API_PUBLIC_URL, og det er også den værdi, API'et bruger, når det selv skal lave en absolut URL. På den instans, vi driver, er det https://api.nessie.works; en udviklingsinstans startet med pnpm dev svarer på http://localhost:5454.

Admin-webappen har en anden oprindelse end API'et, så browserklienter er underlagt en CORS-tilladelsesliste (NESSIE_CORS_ORIGINS). Forespørgsler fra et script eller en server har ingen oprindelse at kontrollere og påvirkes ikke.

Autentificering

Alle forespørgsler uden for et lille sæt bevidst offentlige ruter bærer et adgangstoken i den almindelige header:

Authorization: Bearer <token>

Tokenet er en signeret JWT, som API'et selv udsteder. Det er kortlivet — som standard tredive minutter (NESSIE_AUTH_TOKEN_TTL) — og det er det eneste, en almindelig kalder nogensinde fremviser. Nessie har ingen langlivede personlige API-nøgler. Hvis du vil have et script til at kalde API'et, logger det ind på samme måde som en persons browser og fornyer sit token undervejs.

Hent et token

POST /api/auth/session opretter en session. Det svarer med adgangstokenet og kalderens identitet og sætter fornyelsestokenet som en separat HTTP-only-cookie.

Hvordan du beviser, hvem du er, afhænger af, hvordan installationen er sat op. En installation i local-tilstand accepterer e-mail og adgangskode. På en hosted- eller selfHosted-installation afvises adgangskodelogin helt med 403 PASSWORD_AUTH_DISABLED, og den eneste vej ind er den konfigurerede identitetsudbyder — så kontoen aldrig kan få en anden, svagere identitetsvej ved siden af den.

Log ind via en identitetsudbyder

Single sign-on er en OAuth-udveksling af autorisationskode med PKCE i tre trin. Nessies egen identitetsudbyder er UnlikeOtherAI, som også ejer den organisations- og teamstruktur, en installation spejler; udvekslingen nedenfor har samme form for enhver konfigureret udbyder.

  1. GET /api/auth/providers lister, hvad denne installation accepterer, hver med et providerId, en label og om login-siden automatisk skal sende personen direkte videre til den.
  2. GET /api/auth/providers/:providerId/authorize med codeChallenge, redirectUri og state som query-parametre svarer med { data: { authorizeUrl } }. Send personen derhen.
  3. Udbyderen sender dem tilbage til din redirectUri med en kode. Send den tilbage som POST /api/auth/session med { providerId, code, codeVerifier, redirectUri } — alle fire felter er påkrævet samtidig, og et ufuldstændigt sæt giver 400 EXTERNAL_AUTH_INCOMPLETE.

Forny en session

Fornyelsestokenet står aldrig i et svar-body. Det sættes som en HTTP-only-cookie ved navn nessie_refresh, afgrænset til stien /api/auth, med en levetid på tredive dage som standard (NESSIE_AUTH_REFRESH_TOKEN_TTL). Et kald til POST /api/auth/refresh med den cookie bruger den op, udsteder et nyt adgangstoken og sætter en ny cookie i stedet. Tokenerne roteres, så et stjålet token kun virker, indtil den rigtige klient næste gang fornyer.

En browser, der er logget ind på flere konti, har én sådan cookie pr. login, ved navn nessie_refresh_<slot>, og angiver den, en forespørgsel vedrører, med headeren x-nessie-account-slot. En klient, der ikke sender en sådan header, handler om nessie_refresh, og det er alt, en kommandolinjeklient behøver.

En kommandolinjeklient har derfor brug for en cookie-jar. Med curl er det --cookie-jar på login-kaldet og --cookie på fornyelsen, som i det arbejdede eksempel nedenfor.

Hvad der kontrolleres ved hver forespørgsel

Autentificering er ikke kun en signaturkontrol. Før en handler kører, kontrollerer API'et tokenets signatur og udløbstid, bekræfter at sessionen ikke er tilbagekaldt (både via brugerens token-version og via session-id'et) og indlæser kalderens medlemskab af den organisation, tokenet angiver. Et deaktiveret medlemskab giver en 403. Når organisationen er knyttet til en ekstern identitetsudbyder, tjekkes kalderens aktuelle status hos den udbyder på forespørgslen i stedet for at blive læst fra tokenet, så adgang, der fjernes opstrøms, holder op med at virke med det samme og ikke først ved næste udløb.

Legitimationsoplysninger, der ikke er sessiontokens

Der findes to andre bearer-legitimationsoplysninger, og ingen af dem er en nøgle til almindelig brug. En stemmeenheds-legitimation accepteres kun på stemmeopkaldsruterne, og en agentadgangslegitimation kun på MCP-endpointet. At fremvise en af dem andre steder afvises med en 403, der navngiver legitimationen, og den forsøges ikke igen som en svagere session — se MCP og forbundne værktøjer for, hvordan agentlegitimationen udstedes.

Svarformatet

Hvert vellykket svar er et objekt med en data-nøgle. Der er hverken et nøgent array eller en nyttelast på øverste niveau, så en klient aldrig behøver at skelne på form:

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

Listeendepunkter tilføjer et meta-objekt ved siden af. Intet andet tilføjes nogensinde på øverste niveau.

Fejl bruger også ét format, uanset hvad der forårsagede dem:

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

code er den maskinlæsbare del og er det, du skal forgrene dig på; message er til et menneske. field og details er til stede, når fejlen er specifik nok til at pege på den. En uhåndteret serverfejl er altid 500 INTERNAL_ERROR med en fast besked — den underliggende fejltekst logges, men returneres aldrig, fordi den kan have fanget forespørgselsdata på vej ud.

Fejlkoder, du møder først

StatusKodeBetydning
400VALIDATION_ERRORBody'en eller query'en stemte ikke med skemaet.
401AUTH_REQUIREDIntet bearer-token, eller headeren var ugyldig.
401TOKEN_EXPIREDAdgangstokenet er udløbet. Forny, og prøv igen.
401TOKEN_REVOKEDSessionen blev logget ud, eller alle sessioner blev tvunget ud.
403ACCOUNT_DEACTIVATEDMedlemskabet af denne organisation er blevet deaktiveret.
404NOT_FOUNDIngen sådan rute, eller ingen sådan post, som kalderen kan se.
429RATE_LIMITEDEn bucket er fuld. Læs Retry-After.
500INTERNAL_ERROREn uhåndteret serverfejl, bevidst uden detaljer.

Paginering

Listeendepunkter paginerer med en uigennemsigtig keyset-markør, ikke et offset — rækker indsættes, mens nogen læser, og med et offset gentager den næste side stille og roligt en post, som den første side allerede viste.

Tre query-parametre accepteres: cursor, limit og direction (forward eller backward). Standardsiden er 25, og loftet er 100; en større værdi afkortes i stedet for at blive afvist. Endepunkter, der også sorterer eller filtrerer, tilføjer sort, order og en fritekst-q.

Sidens meta fortæller dig, hvor du er:

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

nextCursor og prevCursor er begge reelle: Forrige og Næste er begge kontroller, en person forventer. De er uigennemsigtige — serveren koder sin egen sorteringsnøgle ind i dem, og en klient, der parser eller konstruerer en, går i stykker, så snart en liste ændrer rækkefølge. total er til stede på de fleste lister og udeladt, hvor optælling ikke giver mening, f.eks. ved rangerede søgeresultater.

Hastighedsgrænser

Grænser gælder pr. fast tidsvindue og håndhæves, før forespørgslens body overhovedet læses. En afvist forespørgsel er 429 med koden RATE_LIMITED og en Retry-After-header i sekunder.

De fleste buckets er knyttet til klientens IP. De legitimationsfølsomme — login, fornyelse, genbekræftelse af adgangskode og skrivninger af MCP-legitimation — har derudover en tæller pr. konto og afviser, når enten slår ud, så én adresse, der sprøjter mange konti, og én konto, der angribes fra mange adresser, begge løber ind i en væg. Login har som standard 10 forsøg pr. IP og 5 pr. e-mailadresse pr. ti minutter; afsendelse af en besked har som standard 60 pr. minut; skrivninger under /api/agents har 60 pr. minut. Hver grænse kan konfigureres pr. installation, så behandl disse tal som de udleverede standardværdier og ikke som en kontrakt. Sundhedstjek og realtidsstrømme er undtaget.

Realtid

At følge en samtale ved at polle en liste er den forkerte måde. I stedet findes tre streaming-endpoints:

  • GET /api/events/stream — server-sent events for alt, den indloggede person kan se.
  • GET /api/threads/:threadId/stream — server-sent events for én tråd. Den respekterer Last-Event-ID, så en genforbindelse genoptager i stedet for at starte forfra.
  • GET /api/activity — en WebSocket, der bærer aktivitetsfeedet.

Browsere kan ikke sætte headers ved et WebSocket-handshake, så WebSocket-ruten accepterer også adgangstokenet som en ?token=-query-parameter. Det er det eneste sted, et token hører hjemme i en URL; alle andre ruter tager det i headeren.

Ressourcefamilierne

Der er langt over tusind endpoints, for mange til at liste. De grupperes i familier, og når du kender en families præfiks, er de enkelte ruter forudsigelige — en samling under præfikset, en post under den og navngivne handlinger som POST-understier.

FamiliePræfiksHvad den dækker
Auth/api/authLogin, fornyelse, logud, den aktuelle identitet og en persons egne sessioner.
Kanaler og tråde/api/channels, /api/threadsSamtalemodellen: kanaler holder tråde, tråde holder beskeder, og svar hænger under en rod.
Agenter/api/agentsAgentdefinitioner, deres kanalbindinger, samtaler, to-do-lister og status.
Projekter og tavler/api/projects, /api/tasks, /api/iterationsOpgavestyring: tavler og kolonner under et projekt, opgaver på dem og iterationer over dem.
Viden/api/knowledge-baseSpaces med versionerede sider og filer, med kommentarer, backlinks og søgning.
Mail/api/mail, /api/mailbox-connectionsForbundne postkasser, agentpostkasser og udkast, en agent har skrevet til en person at godkende.
Godkendelser/api/approvalsKøen af handlinger, en agent ikke må tage på egen hånd, og det ene kald, der afgør dem.
Audit/api/audit-logOrganisationens hash-kædede revisionsspor med et verifikationsendpoint. Ejere og administratorer.
Omkostninger og forbrug/api/ledger, /api/billingToken- og omkostningsregnskab, budgetter og kørselstider; separat kreditter og påfyldninger. Ejere og administratorer.
Organisation og teams/api/organizations, /api/teams, /api/usersAdministration af tenant, team og medlemskaber, inklusive invitationer.
Triggere/api/triggersTidsplaner og webhooks, der starter agentarbejde, med deres udløsningshistorik.
Opkald og stemme/api/calls, /api/voiceOpkald i kanaler og stemmeassistentens session, udskrift og enhedstokens.
Executors/api/executors, /api/execution-environment-templatesDe maskiner, agenter arbejder på, deres miljøer og daemon-parringskanalen.
Kørsler/api/runsKørende agentkørsler: annullér, fortsæt, genstart og bind til en executor.
Uploads/api/uploads, /api/attachmentsUpload en fil først, og knyt derefter det returnerede id til en besked.
Søgning/api/messages/searchBeskedsøgning, for hele organisationen eller afgrænset til én kanal.
Hemmeligheder og indstillinger/api/secrets, /api/settings/scopedSkrivebeskyttet legitimationsopbevaring med eksplicitte tilladelser og lagdelte konfigurationsværdier.
Forbundne værktøjer/api/mcpMCP-servere, denne installation kalder ud til. Se MCP og forbundne værktøjer.
Sundhed/api/healthLiveness og readiness, begge offentlige. /api/health/ready tjekker også databasen.

Rettigheder håndhæves pr. rute, ikke pr. familie. Overordnet set er revisionsloggen, omkostningsregnskabet, politikregler og inferens-kontrolplanen overflader for organisationens ejere og administratorer, mens samtaler, projekter, viden og godkendelser er åbne for ethvert medlem og indsnævret af det, medlemmet faktisk kan se.

Arbejdede eksempler

Log ind og behold fornyelsescookien

På en installation i local-tilstand, med adgangskode. Adgangstokenet kommer tilbage i body'en; fornyelsestokenet havner i cookie-jar'en.

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

Når tokenet udløber, byt cookien for et nyt:

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

Svaret har samme { token, me }-form, og cookie-jar'en holder nu det roterede fornyelsestoken.

List de kanaler, du kan se

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

Tilføj ?teamId=… for at indsnævre til ét team, eller ?includeArchived=true for at medtage arkiverede kanaler. defaultThreadId er den tråd, du poster til næste gang.

Send en besked

En besked skal enten have noget content eller mindst én uploadet vedhæftning. Send en Idempotency-Key, og et forsøg, der gentages, giver den besked, det første forsøg oprettede, i stedet for at poste en anden kopi.

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

En ny besked er 201:

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

En gentagelse med samme idempotensnøgle svarer 200 med den oprindelige besked og en tom pendingAgentInvites, fordi det første forsøg allerede har udført den varsling og afsendelse, som arrayet beskriver.

To afvisninger på denne rute er værd at håndtere eksplicit. Indhold ud over chatlængdegrænsen er 413 MESSAGE_TOO_LARGE, og det rigtige svar er at uploade teksten som en fil i stedet. Indhold, der strukturelt ligner en legitimation, er 422 SECRET_INTERCEPTED — serveren afviser, før beskeden gemmes, afsendes eller logges, og legitimationen hører hjemme i /api/secrets.