nessie

API

HTTP-API:et bakom varje Nessie-klient och hur du autentiserar mot det.

Vad API:t är

Nessie har ett enda HTTP-API. Den webbaserade adminappen, klienterna för macOS och iOS samt fjärrexecutorns daemon är alla vanliga klienter till det, och det de kan göra går inte att nå på något annat sätt. Det är ett JSON-API ovanpå en Fastify-server, det är inte GraphQL, och det har ingen separat ”publik” delmängd: ytan dina egna skript anropar är samma yta som produkten anropar.

Det finns inget versionssegment i sökvägen. Alla rutter ligger under /api, utom den inkommande MCP-slutpunkten POST /mcp och ett fåtal /.well-known/-dokument för upptäckt. Kompatibiliteten hanteras genom att man bara lägger till fält, så att en klient som ignorerar det den inte känner igen fortsätter att fungera.

Bas-URL

Nessie körs på egen server, så bas-URL:en är den adress din driftsättning serverar API:t på. Operatören anger den som NESSIE_API_PUBLIC_URL, och samma värde är vad API:t använder när det behöver skapa en egen absolut URL. På den instans vi kör är det https://api.nessie.works; en utvecklingsinstans som startas med pnpm dev svarar på http://localhost:5454.

Adminappen har en annan origin än API:t, så webbklienter omfattas av en CORS-tillåtelselista (NESSIE_CORS_ORIGINS). Anrop från ett skript eller en server har ingen origin att kontrollera och påverkas inte.

Autentisering

Varje förfrågan utanför en liten uppsättning medvetet öppna rutter bär en åtkomsttoken i den vanliga headern:

Authorization: Bearer <token>

Token är en signerad JWT som utfärdas av API:t. Den har kort livslängd, som standard trettio minuter (NESSIE_AUTH_TOKEN_TTL), och är den enda autentiseringsuppgift en vanlig anropare någonsin visar upp. Nessie har inga långlivade personliga API-nycklar. Om du vill att ett skript ska anropa API:t loggar det in på samma sätt som en persons webbläsare gör och förnyar sin token under tiden.

Hämta en token

POST /api/auth/session skapar en session. Den svarar med åtkomsttoken och anroparens identitet, och sätter förnyelsetoken som en separat HTTP-only-cookie.

Hur du bevisar vem du är beror på hur installationen är driftsatt. En installation som kör i läget local tar emot e-post och lösenord. På en hosted- eller selfHosted-installation nekas inloggning med lösenord helt och hållet med 403 PASSWORD_AUTH_DISABLED, och enda vägen in är den konfigurerade identitetsleverantören. Därmed kan kontot aldrig få en andra, svagare identitetsväg vid sidan av den.

Logga in med en identitetsleverantör

Single sign-on är ett OAuth-utbyte av auktoriseringskod med PKCE, i tre steg. Nessies egen identitetsleverantör är UnlikeOtherAI, som också äger den organisations- och teamstruktur en installation speglar; utbytet nedan har samma form för vilken konfigurerad leverantör som helst.

  1. GET /api/auth/providers listar vad den här instansen accepterar, var och en med ett providerId, en etikett och om inloggningssidan ska skicka vidare dit automatiskt.
  2. GET /api/auth/providers/:providerId/authorize med codeChallenge, redirectUri och state som frågeparametrar svarar med { data: { authorizeUrl } }. Skicka personen dit.
  3. Leverantören skickar tillbaka dem till din redirectUri med en kod. Skicka den som POST /api/auth/session med { providerId, code, codeVerifier, redirectUri }. Alla fyra krävs tillsammans, och en ofullständig uppsättning ger 400 EXTERNAL_AUTH_INCOMPLETE.

Förnya en session

Förnyelsetoken finns aldrig i ett svar. Den sätts som en HTTP-only-cookie med namnet nessie_refresh, begränsad till sökvägen /api/auth, med en giltighetstid på trettio dagar som standard (NESSIE_AUTH_REFRESH_TOKEN_TTL). Om du anropar POST /api/auth/refresh med den cookien förbrukas den, en ny åtkomsttoken utfärdas och en ny cookie sätts i dess ställe. Token roteras, så en stulen token duger bara tills den riktiga klienten förnyar nästa gång.

En webbläsare som är inloggad på flera konton har en sådan cookie per inloggning, med namnet nessie_refresh_<slot>, och anger vilken en förfrågan gäller med headern x-nessie-account-slot. En klient som inte skickar någon sådan header gäller nessie_refresh, vilket är allt en kommandoradsklient behöver.

En kommandoradsklient behöver därför en cookie-jar. Med curl är det --cookie-jar på inloggningsanropet och --cookie på förnyelsen, som i exemplet nedan.

Vad som kontrolleras vid varje förfrågan

Autentisering är inte bara en kontroll av signaturen. Innan en hanterare körs verifierar API:t tokenens signatur och utgångstid, bekräftar att sessionen inte har återkallats (både med användarens tokenversion och med sessionens id) och läser in anroparens medlemskap i den organisation tokenen anger. Ett inaktiverat medlemskap ger en 403. När organisationen backas av en extern identitetsleverantör kontrolleras anroparens aktuella status hos leverantören vid förfrågan i stället för att läsas från tokenen, så att åtkomst som tagits bort uppströms slutar fungera omedelbart i stället för vid nästa utgång.

Autentiseringsuppgifter som inte är sessionstoken

Det finns två andra bearer-autentiseringsuppgifter, och ingen av dem är en nyckel för allmänt bruk. En röstenhets autentiseringsuppgift godtas bara på röstsamtalsrutterna; en agents åtkomstuppgift godtas bara på MCP-slutpunkten. Att visa upp någon av dem någon annanstans nekas med en 403 som anger autentiseringsuppgiften, och den omprövas inte som en svagare session. Se MCP och anslutna verktyg för hur agentens åtkomstuppgift utfärdas.

Svarsformatet

Varje lyckat svar är ett objekt med en nyckel data. Det finns ingen naken lista och ingen nyttolast på rotnivån, så en klient behöver aldrig skilja mellan olika former:

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

Listendpoints lägger till ett objekt meta bredvid den. Inget annat läggs någonsin till på toppnivån.

Fel använder också ett enda format, oavsett vad som orsakade dem:

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

code är den maskinläsbara delen och är det du ska förgrena på; message är till för en person. field och details finns med när felet är tillräckligt specifikt för att kunna lokaliseras. Ett oväntat serverfel är alltid 500 INTERNAL_ERROR med ett fast meddelande. Den underliggande feltexten loggas men returneras aldrig, eftersom den kan ha fångat upp förfrågningsdata på vägen ut.

Felkoder du stöter på först

StatusKodBetydelse
400VALIDATION_ERRORBrödtexten eller frågeparametrarna matchade inte schemat.
401AUTH_REQUIREDIngen bärartoken, eller så var headern felaktigt formad.
401TOKEN_EXPIREDÅtkomsttoken har gått ut. Förnya och försök igen.
401TOKEN_REVOKEDSessionen loggades ut, eller alla sessioner tvingades ut.
403ACCOUNT_DEACTIVATEDMedlemskapet i den här organisationen har inaktiverats.
404NOT_FOUNDIngen sådan rutt, eller ingen sådan post som anroparen kan se.
429RATE_LIMITEDEn gräns är nådd. Läs Retry-After.
500INTERNAL_ERROREtt oväntat serverfel, medvetet ogenomskinligt.

Paginering

Listendpoints paginerar med en ogenomskinlig nyckelbaserad markör, inte med offset. Rader läggs till medan någon läser, och med en offset upprepar andra sidan tyst en post som första sidan redan visade.

Tre frågeparametrar godtas: cursor, limit och direction (forward eller backward). Standardsidan är 25 och taket är 100; ett större värde begränsas i stället för att avvisas. Endpoints som också sorterar eller filtrerar lägger till sort, order och en fritextsökning i q.

Sidans meta visar var du är:

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

nextCursor och prevCursor finns båda. Föregående och Nästa är två kontroller en person förväntar sig. De är ogenomskinliga: servern kodar sin egen sorteringsnyckel i dem, och en klient som tolkar eller konstruerar en av dem slutar fungera så fort en lista ändrar ordning. total finns med på de flesta listor och utelämnas där det inte är meningsfullt att räkna, till exempel vid rankade sökresultat.

Hastighetsbegränsningar

Gränser gäller per fast tidsfönster och tillämpas innan förfrågans brödtext ens har lästs. En avvisad förfrågan är 429 med koden RATE_LIMITED och en header Retry-After i sekunder.

De flesta gränserna knyts till klientens IP-adress. De känsliga för autentiseringsuppgifter, alltså inloggning, förnyelse, ny lösenordsverifiering och skrivningar av MCP-autentiseringsuppgifter, har dessutom en räknare per konto och avvisar när antingen gränsen slår till, så att både en adress som sprider försök över många konton och ett konto som angrips från många adresser når en vägg. Inloggning tillåter som standard 10 försök per IP-adress och 5 per e-postadress var tionde minut; att skicka ett meddelande tillåts 60 per minut; skrivningar under /api/agents tillåts 60 per minut. Varje tröskel kan konfigureras per driftsättning, så läs dessa som de standardvärden som levereras snarare än som ett avtal. Hälsokontroller och realtidsströmmarna är undantagna.

Realtid

Att hämta en lista på nytt hela tiden är fel sätt att följa ett samtal. Det finns i stället tre strömmande endpoints:

  • GET /api/events/stream — server-sent events för allt den inloggade personen kan se.
  • GET /api/threads/:threadId/stream — server-sent events för en tråd. Den respekterar Last-Event-ID, så en återanslutning fortsätter i stället för att börja om.
  • GET /api/activity — en WebSocket som bär aktivitetsflödet.

Webbläsare kan inte sätta headers vid en WebSocket-handskakning, så WebSocket-rutten godtar också åtkomsttoken som frågeparametern ?token=. Det är det enda stället en token hör hemma i en URL; alla andra rutter tar den i headern.

Resursfamiljerna

Det finns långt över tusen endpoints, för många för att listas här. De grupperas i familjer, och när du känner till en familjs prefix är de enskilda rutterna förutsägbara: en samling under prefixet, en post under den och namngivna åtgärder som POST-underspår.

FamiljPrefixVad den omfattar
Autentisering/api/authInloggning, förnyelse, utloggning, den aktuella identiteten och en persons egna sessioner.
Kanaler och trådar/api/channels, /api/threadsSamtalsmodellen: kanaler innehåller trådar, trådar innehåller meddelanden, och svar hänger under en rot.
Agenter/api/agentsAgentdefinitioner, deras kanalbindningar, samtal, att-göra-poster och status.
Projekt och tavlor/api/projects, /api/tasks, /api/iterationsArbetsuppföljning: tavlor och kolumner under ett projekt, ärenden på dem och iterationer över dem.
Kunskapsbas/api/knowledge-baseUtrymmen med versionerade sidor och filer, med kommentarer, bakåtlänkar och sökning.
E-post/api/mail, /api/mailbox-connectionsAnslutna brevlådor, agentbrevlådor och utkast som en agent skrivit för att en person ska godkänna.
Godkännanden/api/approvalsKön med åtgärder en agent inte får ta på egen hand, och det enda anropet som avgör dem.
Granskning/api/audit-logOrganisationens hashkedjade granskningslogg, med en endpoint för verifiering. Ägare och administratörer.
Kostnader och användning/api/ledger, /api/billingToken- och kostnadsredovisning, budgetar och körtider; separat krediter och påfyllningar. Ägare och administratörer.
Organisation och team/api/organizations, /api/teams, /api/usersAdministration av tenant, team och medlemskap, inklusive inbjudningar.
Utlösare/api/triggersScheman och webhooks som startar agentarbete, med deras utlösningshistorik.
Samtal och röst/api/calls, /api/voiceSamtal i kanalen, och röstassistentens session, transkript och enhetstokens.
Executorer/api/executors, /api/execution-environment-templatesMaskinerna agenterna arbetar på, deras miljöer och demonens parkopplingskanal.
Körningar/api/runsPågående agentkörningar: avbryt, fortsätt, starta om och bind till en executor.
Uppladdningar/api/uploads, /api/attachmentsLadda upp en fil först och koppla sedan det returnerade id:t till ett meddelande.
Sök/api/messages/searchMeddelandesökning, för hela organisationen eller begränsad till en kanal.
Hemligheter och inställningar/api/secrets, /api/settings/scopedSkrivskyddad lagring av autentiseringsuppgifter med uttryckliga behörigheter, samt konfigurationsvärden på flera nivåer.
Anslutna verktyg/api/mcpMCP-servrar som den här installationen anropar. Se MCP och anslutna verktyg.
Hälsa/api/healthLiveness och readiness, båda öppna. /api/health/ready kontrollerar också databasen.

Behörigheter tillämpas per rutt, inte per familj. I stora drag: granskningsloggen, kostnadsboken, policyreglerna och kontrollplanet för inferens är ytor för organisationens ägare och administratörer, medan konversationer, projekt, kunskapsbas och godkännanden står öppna för alla medlemmar och begränsas av vad den medlemmen faktiskt kan se.

Exempel

Logga in och behåll förnyelsecookien

På en instans i läget local, med lösenord. Åtkomsttoken kommer tillbaka i brödtexten; förnyelsetoken hamnar i cookie-jarn.

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 token har gått ut byter du cookien mot en ny:

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

Svaret har samma { token, me }-form, och cookie-jarn innehåller nu den roterade förnyelsetoken.

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

Lägg till ?teamId=… för att begränsa till ett team, eller ?includeArchived=true för att ta med arkiverade kanaler. defaultThreadId är det du skickar till härnäst.

Skicka ett meddelande

Ett meddelande behöver antingen något content eller minst en uppladdad bilaga. Skicka en Idempotency-Key, så löses ett upprepat anrop mot det meddelande det första försöket skapade i stället för att skapa en andra kopia.

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

Ett nytt meddelande ger 201:

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

Ett upprepat anrop med samma idempotensnyckel svarar 200 med det ursprungliga meddelandet och en tom pendingAgentInvites, eftersom det första försöket redan gjort den aviseringen och utsändningen som arrayen beskriver.

Två avvisningar på den här rutten bör hanteras uttryckligen. Innehåll som överskrider chattens längdgräns ger 413 MESSAGE_TOO_LARGE, och rätt svar är att ladda upp texten som en fil i stället. Innehåll som strukturellt liknar en autentiseringsuppgift ger 422 SECRET_INTERCEPTED. Servern avvisar det innan meddelandet lagras, skickas ut eller loggas, och autentiseringsuppgiften hör hemma i /api/secrets.