nessie

API

HTTP-API-et bak hver Nessie-klient, og hvordan du autentiserer deg mot det.

Hva API-et er

Nessie har ett HTTP-API. Admin-webappen, macOS- og iOS-klientene og den eksterne executor-demonen er alle vanlige klienter av det, og det de kan gjøre, kan ikke nås på noen annen måte. Det er et JSON-API over en Fastify-server. Det er ikke GraphQL, og det finnes ingen egen «offentlig» delmengde: flaten egne skript kaller, er den produktet kaller.

Stien har ikke noe versjonssegment. Alle ruter ligger under /api, bortsett fra det innkommende MCP-endepunktet POST /mcp og en håndfull /.well-known/-dokumenter for oppdagelse. Kompatibilitet ivaretas ved bare å legge til felt, slik at en klient som ignorerer det den ikke gjenkjenner, fortsetter å fungere.

Basis-URL

Nessie kjøres på egen infrastruktur, så basis-URL-en er den opprinnelsen driftsmiljøet ditt serverer API-et fra. Driftsansvarlig setter den som NESSIE_API_PUBLIC_URL, og den samme verdien er det API-et bruker når det må lage en egen absolutt URL. På instansen vi kjører, er det https://api.nessie.works. En utviklingsinstans startet med pnpm dev svarer på http://localhost:5454.

Admin-webappen har en annen opprinnelse enn API-et, så nettleserklienter er underlagt en CORS-tillatelsesliste (NESSIE_CORS_ORIGINS). Forespørsler fra et skript eller en server har ingen opprinnelse å sjekke, og påvirkes ikke.

Autentisering

Alle forespørsler utenfor et lite sett med bevisst offentlige ruter bærer et tilgangstoken i standard HTTP-header:

Authorization: Bearer <token>

Tokenet er en signert JWT utstedt av API-et. Det er kortlevd: tretti minutter som standard (NESSIE_AUTH_TOKEN_TTL). Det er den eneste påloggingsopplysningen en vanlig kaller noen gang presenterer. Nessie har ingen langlivede personlige API-nøkler. Hvis du vil at et skript skal kalle API-et, logger det inn på samme måte som nettleseren til en person og fornyer tokenet underveis.

Få et token

POST /api/auth/session oppretter en økt. Svaret inneholder tilgangstokenet og identiteten til kalleren, og fornyelsestokenet settes som en egen HTTP-only-informasjonskapsel.

Hvordan du beviser hvem du er, avhenger av hvordan installasjonen er satt opp. En installasjon som kjører i local-modus, godtar e-post og passord. På en hosted- eller selfHosted-installasjon blir innlogging med passord avvist rett ut med 403 PASSWORD_AUTH_DISABLED, og eneste vei inn er den konfigurerte identitetsleverandøren. Da kan kontoen aldri få en annen, svakere identitetsvei ved siden av den.

Logge inn via en identitetsleverandør

Single sign-on er en OAuth-utveksling med autorisasjonskode og PKCE, i tre steg. Nessies egen identitetsleverandør er UnlikeOtherAI, som også eier organisasjons- og teamstrukturen en installasjon speiler. Utvekslingen under har samme form for alle konfigurerte leverandører.

  1. GET /api/auth/providers lister hva denne installasjonen godtar, med en providerId, en etikett og om innloggingssiden skal sende brukeren dit automatisk.
  2. GET /api/auth/providers/:providerId/authorize med codeChallenge, redirectUri og state som query-parametere, svarer med { data: { authorizeUrl } }. Send personen dit.
  3. Leverandøren sender personen tilbake til redirectUri med en kode. Send den tilbake som POST /api/auth/session med { providerId, code, codeVerifier, redirectUri }. Alle fire feltene må være med samtidig, og et ufullstendig sett gir 400 EXTERNAL_AUTH_INCOMPLETE.

Fornye en økt

Fornyelsestokenet står aldri i et svar. Det settes som en HTTP-only-informasjonskapsel med navnet nessie_refresh, begrenset til stien /api/auth, med en levetid på tretti dager som standard (NESSIE_AUTH_REFRESH_TOKEN_TTL). Et kall til POST /api/auth/refresh med den informasjonskapselen bruker den opp, utsteder et nytt tilgangstoken og setter en ny informasjonskapsel i stedet. Tokenene roteres, så et stjålet token gjelder bare til den ekte klienten fornyer neste gang.

En nettleser som er logget inn på flere kontoer, har én slik informasjonskapsel per innlogging, med navnet nessie_refresh_<slot>, og oppgir hvilken en forespørsel gjelder, med headeren x-nessie-account-slot. En klient som ikke sender en slik header, gjelder nessie_refresh, og det er alt en kommandolinjeklient trenger.

En kommandolinjeklient trenger derfor en informasjonskapselfil (cookie jar). Med curl er det --cookie-jar på innloggingskallet og --cookie ved fornyelsen, som i eksemplet nedenfor.

Hva som kontrolleres på hver forespørsel

Autentisering er ikke bare en signatursjekk. Før en behandler kjører, verifiserer API-et tokenets signatur og utløpstid, bekrefter at økten ikke er tilbakekalt (både etter tokenversjonen for hele brukeren og etter den enkelte økt-ID-en), og laster kallerens medlemskap i organisasjonen tokenet oppgir. Et deaktivert medlemskap gir en 403. Når organisasjonen har en ekstern identitetsleverandør, sjekkes kallerens nåværende status hos den leverandøren på forespørselen, i stedet for å leses fra tokenet. Tilgang som er fjernet hos leverandøren, slutter dermed å virke med en gang, ikke først ved neste utløp.

Påloggingsopplysninger som ikke er øktokener

Det finnes to andre bearer-påloggingsopplysninger, og ingen av dem er en nøkkel til generell bruk. En stemmeenhets-påloggingsopplysning godtas bare på stemmesamtalerutene. En agenttilgangs-påloggingsopplysning godtas bare på MCP-endepunktet. Å presentere en av dem andre steder avvises med en 403 som nevner påloggingsopplysningen, og den prøves ikke på nytt som en svakere økt. Se MCP og tilkoblede verktøy for hvordan agentpåloggingsopplysningen utstedes.

Svarformatet

Hvert vellykket svar er et objekt med en data-nøkkel. Det finnes ingen ren liste og ingen nyttelast på øverste nivå, så en klient aldri trenger å skille på form:

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

Listeendepunkter legger til et meta-objekt ved siden av. Ingen andre felt legges til på øverste nivå.

Feil bruker også ett format, uansett hva som forårsaket dem:

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

code er den maskinlesbare delen, og det er den du skal forgrene på. message er for et menneske. field og details er med når feilen er konkret nok til å peke ut. En ubehandlet serverfeil er alltid 500 INTERNAL_ERROR med en fast melding. Den underliggende feilteksten logges, men returneres aldri, fordi den kan ha fanget opp forespørselsdata på vei ut.

Feilkoder du møter først

StatusKodeBetydning
400VALIDATION_ERRORInnholdet eller spørringen samsvarte ikke med skjemaet.
401AUTH_REQUIREDMangler bearer-token, eller headeren er feilformatert.
401TOKEN_EXPIREDTilgangstokenet har utløpt. Forny det og prøv på nytt.
401TOKEN_REVOKEDØkten ble logget ut, eller alle økter ble tvunget ut.
403ACCOUNT_DEACTIVATEDMedlemskapet i denne organisasjonen er deaktivert.
404NOT_FOUNDIngen slik rute, eller ingen slik post som er synlig for kalleren.
429RATE_LIMITEDGrensen er nådd. Les Retry-After.
500INTERNAL_ERROREn ubehandlet serverfeil, bevisst uten detaljer.

Paginering

Listeendepunkter pagineres med en ugjennomsiktig markør (cursor) basert på nøkkel, ikke med en forskyvning (offset). Rader settes inn mens noen leser, og med en forskyvning gjentar andre side stille en post som første side allerede viste.

Tre query-parametere godtas: cursor, limit og direction (forward eller backward). Standardsiden har 25 poster, og taket er 100. En større verdi begrenses i stedet for å avvises. Endepunkter som også sorterer eller filtrerer, legger til sort, order og et fritekstfelt q.

meta på siden forteller deg hvor du er:

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

nextCursor og prevCursor er begge ekte: Forrige og Neste er to kontroller en person forventer. De er ugjennomsiktige. Serveren koder sin egen sorteringsnøkkel inn i dem, og en klient som tolker eller bygger en av dem, går i stykker i det øyeblikket en liste endrer rekkefølge. total finnes på de fleste lister, og utelates der telling ikke gir mening, for eksempel i rangerte søkeresultater.

Hastighetsbegrensninger

Grensene gjelder per fast tidsvindu og håndheves før forespørselens innhold i det hele tatt leses. En avvist forespørsel er 429 med koden RATE_LIMITED og en Retry-After-header i sekunder.

De fleste tellegrensene er knyttet til klientens IP-adresse. De som er følsomme for påloggingsopplysninger, nemlig innlogging, fornyelse, ny bekreftelse av passord og skriving av MCP-påloggingsopplysninger, har i tillegg en teller per konto, og avviser når begge utløses. Dermed møter både én adresse som sprer forsøk over mange kontoer, og én konto som angripes fra mange adresser, en vegg. Innlogging har som standard 10 forsøk per IP og 5 per e-postadresse per ti minutter. Å sende en melding har som standard 60 per minutt, og skriving under /api/agents har 60 per minutt. Alle terskler kan konfigureres per driftsmiljø, så regn disse som leveringsstandardene, ikke som en avtale. Helsesjekker og sanntidsstrømmene er unntatt.

Sanntid

Å spørre en liste om og om igjen er feil måte å følge en samtale på. Det finnes tre strømmeendepunkter i stedet:

  • GET /api/events/stream — server-sendte hendelser for alt den innloggede personen kan se.
  • GET /api/threads/:threadId/stream — server-sendte hendelser for én tråd. Den respekterer Last-Event-ID, så en ny tilkobling fortsetter i stedet for å starte på nytt.
  • GET /api/activity — en WebSocket som bærer aktivitetsstrømmen.

Nettlesere kan ikke sette headere i et WebSocket-håndtrykk, så WebSocket-ruten godtar også tilgangstokenet som en ?token=-query-parameter. Det er det eneste stedet et token hører hjemme i en URL. Alle andre ruter tar det i headeren.

Ressursfamiliene

Det finnes godt over tusen endepunkter, altfor mange til å liste opp. De er gruppert i familier, og når du kjenner en familie sitt prefiks, er de enkelte rutene forutsigbare: en samling under prefikset, en post under den, og navngitte handlinger som POST-understier.

FamiliePrefiksHva den dekker
Innlogging/api/authInnlogging, fornyelse, utlogging, gjeldende identitet og en persons egne økter.
Kanaler og tråder/api/channels, /api/threadsSamtalemodellen: kanaler har tråder, tråder har meldinger, og svar ligger under en rotmelding.
Agenter/api/agentsAgentdefinisjoner, kanalkoblingene deres, samtaler, gjøremål og status.
Prosjekter og tavler/api/projects, /api/tasks, /api/iterationsOppgavesporing: tavler og kolonner under et prosjekt, oppgaver på dem, og iterasjoner over disse.
Kunnskap/api/knowledge-baseOmråder med versjonerte sider og filer, med kommentarer, tilbakelenker og søk.
E-post/api/mail, /api/mailbox-connectionsTilkoblede postkasser, agentpostkasser og utkast en agent har skrevet for at en person skal godkjenne.
Godkjenninger/api/approvalsKøen av handlinger en agent ikke kan utføre på egen hånd, og det ene kallet som avgjør dem.
Revisjon/api/audit-logOrganisasjonens hash-kjedede revisjonslogg, med et endepunkt for verifisering. Eiere og administratorer.
Kostnad og bruk/api/ledger, /api/billingRegnskap for tokens og kostnader, budsjetter og kjøretider; i tillegg kreditt og påfyll. Eiere og administratorer.
Organisasjon og team/api/organizations, /api/teams, /api/usersAdministrasjon av leietaker, team og medlemskap, inkludert invitasjoner.
Utløsere/api/triggersTidsplaner og webhooks som starter agentarbeid, med historikken over utløsningene.
Samtaler og stemme/api/calls, /api/voiceSamtaler i kanalen, og stemmeassistentens økt, transkripsjon og enhetstokener.
Executorer/api/executors, /api/execution-environment-templatesMaskinene agentene arbeider på, deres miljøer, og paringskanalen for demonen.
Kjøringer/api/runsAktive agentkjøringer: avbryt, fortsett, start på nytt, knytt til en executor.
Opplastinger/api/uploads, /api/attachmentsLast opp en fil først, og knytt deretter den returnerte ID-en til en melding.
Søk/api/messages/searchSøk i meldinger, for hele organisasjonen eller avgrenset til én kanal.
Hemmeligheter og innstillinger/api/secrets, /api/settings/scopedLagring av påloggingsopplysninger som bare kan skrives til, med eksplisitte tilganger, og lagdelte konfigurasjonsverdier.
Tilkoblede verktøy/api/mcpMCP-servere denne installasjonen kaller ut til. Se MCP og tilkoblede verktøy.
Helsesjekk/api/healthAktivitets- og klarsjekk, begge offentlige. /api/health/ready sjekker også databasen.

Rettigheter håndheves per rute, ikke per familie. I hovedtrekk: revisjonsloggen, kostnadsregnskapet, regelsettene og kontrollplanet for inferens er flater for organisasjonens eiere og administratorer, mens samtaler, prosjekter, kunnskap og godkjenninger er åpne for alle medlemmer og begrenses av det det enkelte medlemmet faktisk kan se.

Eksempler

Logg inn og behold fornyelsesinformasjonskapselen

På en installasjon i local-modus, med passord. Tilgangstokenet kommer tilbake i svaret, og fornyelsestokenet havner i informasjonskapselfilen.

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 utløper, bytter du informasjonskapselen mot et nytt:

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 informasjonskapselfilen inneholder nå det roterte fornyelsestokenet.

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

Legg til ?teamId=… for å avgrense til ett team, eller ?includeArchived=true for å ta med arkiverte kanaler. defaultThreadId er tråden du poster til neste gang.

Send en melding

En melding trenger enten noe content eller minst ett opplastet vedlegg. Send en Idempotency-Key, så løser et nytt forsøk seg til meldingen det første forsøket opprettet, i stedet for å legge inn en ny 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 melding er 201:

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

Et nytt forsøk med samme Idempotency-Key svarer med 200, den opprinnelige meldingen og en tom pendingAgentInvites, fordi første forsøk allerede sto for varslingen og utsendelsen som matrisen beskriver.

To avvisninger på denne ruten er verdt å håndtere eksplisitt. Innhold som overskrider lengdegrensen i chatten, gir 413 MESSAGE_TOO_LARGE, og riktig svar er å laste opp teksten som en fil i stedet. Innhold som strukturelt ser ut som en påloggingsopplysning, gir 422 SECRET_INTERCEPTED. Serveren avviser meldingen før den lagres, sendes eller logges, og påloggingsopplysningen hører hjemme i /api/secrets.