nessie

API

L’API HTTP que hi ha darrere de cada client de Nessie i com t’hi autentiques.

Què és l’API

Nessie té una sola API HTTP. El client web d’administració, els clients de macOS i iOS i el dimoni de l’executor remot són clients ordinaris seus, i res del que poden fer no és accessible d’una altra manera. És una API JSON sobre un servidor Fastify, no és GraphQL, i no té cap subconjunt «públic» a part: la superfície que crides des dels teus propis scripts és la mateixa que utilitza el producte.

No hi ha cap segment de versió al camí. Totes les rutes són sota /api, tret de l’endpoint MCP d’entrada POST /mcp i d’uns quants documents de descobriment /.well-known/. La compatibilitat es gestiona afegint només camps nous, de manera que un client que ignora el que no reconeix continua funcionant.

URL base

Nessie s’allotja en un servidor propi, de manera que la URL base és l’origen on el teu desplegament serveix l’API: l’operador la defineix com a NESSIE_API_PUBLIC_URL, i és el mateix valor que l’API utilitza quan ha de generar una URL absoluta pròpia. A la instància que executem nosaltres, és https://api.nessie.works; una instància de desenvolupament iniciada amb pnpm dev respon a http://localhost:5454.

L’aplicació web d’administració és un origen diferent del de l’API, de manera que els clients del navegador estan subjectes a una llista permesa de CORS (NESSIE_CORS_ORIGINS). Les sol·licituds d’un script o d’un servidor no tenen cap origen que comprovar i no hi estan afectades.

Autenticació

Cada sol·licitud fora d’un petit conjunt de rutes deliberadament públiques porta un token d’accés a la capçalera estàndard:

Authorization: Bearer <token>

El token és un JWT signat que emet l’API. És de vida curta —trenta minuts per defecte (NESSIE_AUTH_TOKEN_TTL)— i és l’única credencial que presenta un client ordinari. Nessie no té claus d’API personals de llarga durada. Si vols que un script cridi l’API, inicia la sessió de la mateixa manera que ho fa el navegador d’una persona i renova el token a mesura que avança.

Obtenir un token

POST /api/auth/session crea una sessió. Respon amb el token d’accés i la identitat de qui fa la crida, i defineix el token de renovació com una cookie HTTP-only independent.

La manera de demostrar qui ets depèn de com s’hagi desplegat la instància. Una instància que s’executa en mode local accepta un correu electrònic i una contrasenya. En una instància hosted o selfHosted, l’inici de sessió amb contrasenya es rebutja directament amb 403 PASSWORD_AUTH_DISABLED, i l’única manera d’entrar és el proveïdor d’identitat configurat; així, el compte no pot tenir mai un segon camí d’identitat, més feble, al costat.

Inici de sessió amb un proveïdor d’identitat

L’inici de sessió únic (SSO) és un intercanvi de codi d’autorització OAuth amb PKCE, en tres passos. El proveïdor d’identitat propi de Nessie és UnlikeOtherAI, que també és el propietari de l’estructura d’organitzacions i equips que reflecteix una instància; l’intercanvi següent té la mateixa forma per a qualsevol proveïdor configurat.

  1. GET /api/auth/providers enumera els proveïdors que accepta aquesta instància, cadascun amb un providerId, una etiqueta i si la pàgina d’inici de sessió ha de redirigir-hi automàticament.
  2. GET /api/auth/providers/:providerId/authorize, amb codeChallenge, redirectUri i state com a paràmetres de consulta, respon amb { data: { authorizeUrl } }. Envia la persona allà.
  3. El proveïdor torna la persona a la teva redirectUri amb un codi. Envia’l de nou com a POST /api/auth/session amb { providerId, code, codeVerifier, redirectUri }: els quatre camps són obligatoris junts, i un conjunt incomplet és un 400 EXTERNAL_AUTH_INCOMPLETE.

Renovar una sessió

El token de renovació mai no és al cos d’una resposta. Es defineix com una cookie HTTP-only anomenada nessie_refresh, amb abast a la ruta /api/auth i una durada de trenta dies per defecte (NESSIE_AUTH_REFRESH_TOKEN_TTL). Si crides POST /api/auth/refresh amb aquesta cookie, la consumeix, emet un token d’accés nou i defineix una cookie nova al seu lloc. Els tokens canvien en cada renovació, de manera que un token robat només és vàlid fins que el client real es renova.

Un navegador amb la sessió iniciada en diversos comptes guarda una cookie d’aquest tipus per a cada inici de sessió, anomenada nessie_refresh_<slot>, i indica a quina es refereix una sol·licitud amb la capçalera x-nessie-account-slot. Un client que no envia aquesta capçalera es refereix a nessie_refresh, que és tot el que necessita un client de línia d’ordres.

Un client de línia d’ordres necessita, doncs, un magatzem de cookies. Amb curl, això vol dir --cookie-jar a la crida d’inici de sessió i --cookie a la renovació, tal com es veu a l’exemple pràctic de més avall.

Què es comprova a cada sol·licitud

L’autenticació no és només una comprovació de signatura. Abans que s’executi un controlador, l’API verifica la signatura i la caducitat del token, confirma que la sessió no s’ha revocat (tant per la versió de token de tot l’usuari com per l’identificador individual de la sessió) i carrega la pertinença de qui fa la crida a l’organització que indica el token. Una pertinença desactivada és un 403. Quan l’organització es basa en un proveïdor d’identitat extern, la situació actual de qui fa la crida es torna a comprovar amb aquest proveïdor en la sol·licitud, en lloc de llegir-la del token; així, un accés retirat en origen deixa de funcionar a l’instant i no només en la següent caducitat.

Credencials que no són tokens de sessió

Existeixen dues altres credencials de tipus bearer, i cap no és una clau d’ús general. Una credencial de dispositiu de veu només s’accepta a les rutes de trucades de veu, i una credencial d’accés d’agent només s’accepta a l’endpoint MCP. Presentar-ne qualsevol en un altre lloc es rebutja amb un 403 que n’indica el nom, i no es reintenta com una sessió més feble. Consulta MCP i eines connectades per saber com s’emet la credencial de l’agent.

L’envoltori de resposta

Cada resposta correcta és un objecte amb una clau data. No hi ha cap matriu sola ni cap càrrega útil a l’arrel, de manera que un client mai no ha de distingir per la forma:

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

Els endpoints de llista afegeixen un objecte meta al costat. No s’hi afegeix res més al nivell superior.

Les fallades també fan servir una sola forma, sigui quina sigui la causa:

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

code és la part llegible per una màquina i és la que has d’utilitzar per decidir; message és per a una persona. field i details hi són quan la fallada és prou concreta per localitzar-la. Un error de servidor no gestionat és sempre 500 INTERNAL_ERROR amb un missatge fix: el text de l’error original es registra, però mai no es retorna, perquè pot haver capturat dades de la sol·licitud pel camí.

Codis d’error que trobaràs primer

EstatCodiSignificat
400VALIDATION_ERROREl cos o la consulta no coincideixen amb l’esquema.
401AUTH_REQUIREDFalta el token bearer, o la capçalera està malformada.
401TOKEN_EXPIREDEl token d’accés ha caducat. Renova’l i torna-ho a provar.
401TOKEN_REVOKEDLa sessió s’ha tancat, o s’han expulsat totes les sessions.
403ACCOUNT_DEACTIVATEDLa pertinença a aquesta organització s’ha desactivat.
404NOT_FOUNDNo existeix la ruta, o no hi ha cap registre visible per a qui fa la crida.
429RATE_LIMITEDS’ha esgotat un límit. Llegeix Retry-After.
500INTERNAL_ERRORUn error de servidor no gestionat, deliberadament opac.

Paginació

Els endpoints de llista es paginen amb un cursor de claus opac, no amb un desplaçament: les files s’insereixen mentre algú les llegeix, i amb un desplaçament la segona pàgina repeteix sense avisar un registre que ja mostrava la primera.

S’accepten tres paràmetres de consulta: cursor, limit i direction (forward o backward). La pàgina per defecte té 25 elements i el màxim és 100; un valor més gran es limita en lloc de rebutjar-se. Els endpoints que també ordenen o filtren hi afegeixen sort, order i un q de text lliure.

El meta de la pàgina indica on ets:

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

nextCursor i prevCursor són tots dos reals: Anterior i Següent són controls que una persona espera. Són opacs —el servidor hi codifica la seva pròpia clau d’ordenació—, i un client que n’analitza o en construeix un es trencarà en el moment en què una llista canviï la seva ordenació. total hi és a la majoria de llistes i s’omet on comptar no té sentit, com en els resultats de cerca classificats.

Límits de peticions

Els límits són per finestra fixa i s’apliquen abans que es llegeixi el cos de la sol·licitud. Una sol·licitud rebutjada és un 429 amb el codi RATE_LIMITED i una capçalera Retry-After en segons.

La majoria de límits es calculen per l’adreça IP del client; els sensibles a les credencials —l’inici de sessió, la renovació, la reverificació de la contrasenya i les escriptures de credencials MCP— mantenen també un comptador per compte i rebutgen si qualsevol dels dos s’activa, de manera que tant una adreça que prova molts comptes com un compte atacat des de moltes adreces topen amb un mur. L’inici de sessió té per defecte 10 intents per IP i 5 per adreça de correu electrònic cada deu minuts; publicar un missatge, 60 per minut; i les escriptures sota /api/agents, 60 per minut. Cada llindar es pot configurar per desplegament, de manera que has de tractar aquests valors com a predeterminats del producte i no com un contracte. Les comprovacions de salut i els fluxos en temps real estan exempts.

Temps real

Consultar una llista de manera repetida és la manera equivocada de seguir una conversa. En canvi, hi ha tres endpoints de flux:

  • GET /api/events/stream — esdeveniments enviats pel servidor de tot el que la persona que ha iniciat la sessió pot veure.
  • GET /api/threads/:threadId/stream — esdeveniments enviats pel servidor per a un sol fil. Respecta Last-Event-ID, de manera que en reconnectar es reprèn en lloc de tornar a començar.
  • GET /api/activity — un WebSocket que transporta el canal d’activitat.

Els navegadors no poden definir capçaleres en el handshake d’un WebSocket, de manera que la ruta WebSocket també accepta el token d’accés com a paràmetre de consulta ?token=. És l’únic lloc on un token pertany a una URL; totes les altres rutes el reben a la capçalera.

Les famílies de recursos

Hi ha molt més de mil endpoints, massa per enumerar-los. S’agrupen en famílies, i un cop saps el prefix d’una família, les rutes individuals són previsibles: una col·lecció sota el prefix, un registre a sota i les accions amb nom com a subrutes POST.

FamíliaPrefixQuè inclou
Autenticació/api/authInici de sessió, renovació, tancament de sessió, la identitat actual i les sessions pròpies d’una persona.
Canals i fils/api/channels, /api/threadsEl model de converses: els canals contenen fils, els fils contenen missatges, i les respostes s’agrupen sota un missatge arrel.
Agents/api/agentsDefinicions d’agents, les seves vinculacions de canal, les converses, les tasques pendents i l’estat.
Projectes i taulers/api/projects, /api/tasks, /api/iterationsSeguiment del treball: taulers i columnes sota un projecte, tasques a sobre i iteracions sobre aquestes.
Coneixement/api/knowledge-baseEspais de pàgines i fitxers versionats, amb comentaris, retroenllaços i cerca.
Correu/api/mail, /api/mailbox-connectionsBústies connectades, bústies d’agents i esborranys que un agent ha escrit perquè una persona els aprovi.
Aprovacions/api/approvalsLa cua d’accions que un agent no pot fer per iniciativa pròpia, i la única crida que les resol.
Auditoria/api/audit-logEl registre d’auditoria de l’organització, amb encadenament de hashes i un endpoint de verificació. Propietaris i administradors.
Cost i ús/api/ledger, /api/billingComptabilitat de tokens i costos, pressupostos i temps d’execució; per separat, crèdits i recàrregues. Propietaris i administradors.
Organització i equips/api/organizations, /api/teams, /api/usersAdministració de l’organització, dels equips i de les pertinences, incloses les invitacions.
Disparadors/api/triggersProgramacions i webhooks que inicien el treball dels agents, amb el seu historial d’activacions.
Trucades i veu/api/calls, /api/voiceLes trucades dins d’un canal, i la sessió, la transcripció i els tokens de dispositiu de l’assistent de veu.
Executors/api/executors, /api/execution-environment-templatesLes màquines on treballen els agents, els seus entorns i el canal d’emparellament del dimoni.
Execucions/api/runsExecucions d’agents en curs: cancel·lar, continuar, reiniciar, vincular a un executor.
Pujades/api/uploads, /api/attachmentsPuja primer un fitxer i després enllaça l’identificador retornat a un missatge.
Cerca/api/messages/searchCerca de missatges, a tota l’organització o limitada a un canal.
Secrets i configuració/api/secrets, /api/settings/scopedEmmagatzematge de credencials només d’escriptura amb concessions explícites, i valors de configuració en capes.
Eines connectades/api/mcpServidors MCP als quals aquesta instància es connecta. Consulta MCP i eines connectades.
Salut/api/healthVitalitat i preparació, totes dues públiques. /api/health/ready també comprova la base de dades.

Els drets s’apliquen per ruta, no per família. En termes generals: el registre d’auditoria, el registre de costos, les regles de política i el pla de control d’inferència són superfícies per als propietaris i administradors de l’organització, mentre que les converses, els projectes, el coneixement i les aprovacions estan oberts a qualsevol membre i es limiten a allò que aquell membre pot veure realment.

Exemples pràctics

Iniciar sessió i guardar la cookie de renovació

En una instància en mode local, amb contrasenya. El token d’accés torna al cos; el token de renovació arriba al magatzem de cookies.

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

Quan caduqui el token, intercanvia la cookie per una de nova:

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

La resposta té la mateixa forma { token, me }, i el magatzem de cookies ara conté el token de renovació rotat.

Llista els canals que pots veure

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

Afegeix ?teamId=… per limitar-ho a un equip, o ?includeArchived=true per incloure els canals arxivats. El defaultThreadId és on publiques el següent missatge.

Publicar un missatge

Un missatge necessita algun content o almenys un fitxer adjunt pujat. Envia una Idempotency-Key i una sol·licitud reintentada es resol amb el missatge que va crear el primer intent, en lloc de publicar una còpia més.

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

Un missatge nou és un 201:

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

Una repetició amb la mateixa clau d’idempotència respon amb un 200, amb el missatge original i un pendingAgentInvites buit, perquè el primer intent ja va fer les alertes i l’enviament que descriu la matriu.

Hi ha dos rebutjos en aquesta ruta que val la pena gestionar explícitament. El contingut que supera el límit de longitud del xat és un 413 MESSAGE_TOO_LARGE, i la resposta adequada és pujar el text com a fitxer. El contingut que estructuralment sembla una credencial és un 422 SECRET_INTERCEPTED: el servidor el rebutja abans que es guardi, s’enviï o es registri, i la credencial pertany a /api/secrets.