nessie

API

La API HTTP que hay detrás de cada cliente de Nessie y cómo autenticarse en ella.

Qué es la API

Nessie tiene una sola API HTTP. La aplicación web de administración, los clientes para macOS e iOS y el daemon del ejecutor remoto son clientes normales de ella, y nada de lo que pueden hacer es accesible de otra forma. Es una API JSON sobre un servidor Fastify, no es GraphQL, y no tiene un subconjunto «público» aparte: la superficie que llaman tus propios scripts es la misma que llama el producto.

La ruta no incluye ningún segmento de versión. Todas las rutas cuelgan de /api, salvo el endpoint MCP de entrada en POST /mcp y unos pocos documentos de descubrimiento bajo /.well-known/. La compatibilidad se mantiene añadiendo siempre campos nuevos, de modo que un cliente que ignora lo que no reconoce sigue funcionando.

URL base

Nessie es de alojamiento propio, así que la URL base es el origen en el que tu despliegue sirve la API. El operador la define como NESSIE_API_PUBLIC_URL, y ese mismo valor es el que usa la API cuando necesita construir una URL absoluta propia. En la instancia que gestionamos nosotros es https://api.nessie.works; una instancia de desarrollo iniciada con pnpm dev responde en http://localhost:5454.

La aplicación web de administración tiene un origen distinto al de la API, así que los clientes del navegador están sujetos a una lista de orígenes permitidos de CORS (NESSIE_CORS_ORIGINS). Las peticiones de un script o de un servidor no tienen origen que comprobar y no se ven afectadas.

Autenticación

Toda petición fuera de un pequeño conjunto de rutas deliberadamente públicas lleva un token de acceso en la cabecera estándar:

Authorization: Bearer <token>

El token es un JWT firmado que emite la API. Tiene una vida corta —treinta minutos por defecto (NESSIE_AUTH_TOKEN_TTL)— y es la única credencial que presenta un cliente normal. Nessie no tiene claves de API personales de larga duración. Si quieres que un script llame a la API, inicia sesión igual que lo hace el navegador de una persona y renueva su token a medida que avanza.

Obtener un token

POST /api/auth/session crea una sesión. Responde con el token de acceso y la identidad de quien llama, y establece el token de renovación en una cookie aparte, HTTP-only.

Cómo demuestras quién eres depende de cómo esté desplegada la instancia. Una instancia en modo local acepta correo electrónico y contraseña. En una instancia hosted o selfHosted, el inicio de sesión con contraseña se rechaza de plano con 403 PASSWORD_AUTH_DISABLED, y la única vía de entrada es el proveedor de identidad configurado, de modo que la cuenta nunca puede tener una segunda vía de identidad, más débil, junto a él.

Iniciar sesión con un proveedor de identidad

El inicio de sesión único es un intercambio de código de autorización de OAuth con PKCE, en tres pasos. El proveedor de identidad de Nessie es UnlikeOtherAI, que también gestiona la estructura de organizaciones y equipos que refleja una instancia; el intercambio siguiente tiene la misma forma para cualquier proveedor configurado.

  1. GET /api/auth/providers lista los proveedores que acepta esta instancia, cada uno con su providerId, una etiqueta y si la página de inicio de sesión debe redirigir automáticamente a él.
  2. GET /api/auth/providers/:providerId/authorize, con codeChallenge, redirectUri y state como parámetros de consulta, responde con { data: { authorizeUrl } }. Lleva a la persona hasta allí.
  3. El proveedor devuelve a la persona a tu redirectUri con un código. Envía ese código de vuelta como POST /api/auth/session con { providerId, code, codeVerifier, redirectUri }; los cuatro campos son obligatorios a la vez, y un conjunto incompleto devuelve 400 EXTERNAL_AUTH_INCOMPLETE.

Renovar una sesión

El token de renovación nunca aparece en el cuerpo de una respuesta. Se establece como cookie HTTP-only llamada nessie_refresh, limitada a la ruta /api/auth, con una duración de treinta días por defecto (NESSIE_AUTH_REFRESH_TOKEN_TTL). Llamar a POST /api/auth/refresh con esa cookie consume el token, emite un token de acceso nuevo y coloca en su lugar una cookie nueva: los tokens rotan, así que uno robado solo sirve hasta que el cliente real vuelva a renovarse.

Un navegador con varias cuentas abiertas guarda una cookie de ese tipo por cada inicio de sesión, llamada nessie_refresh_<slot>, e indica a cuál se refiere una petición con la cabecera x-nessie-account-slot. Un cliente que no envía esa cabecera se refiere a nessie_refresh, que es todo lo que necesita un cliente de línea de comandos.

Por tanto, un cliente de línea de comandos necesita un almacén de cookies. Con curl eso significa --cookie-jar en la llamada de inicio de sesión y --cookie en la de renovación, como en el ejemplo práctico de más abajo.

Qué se comprueba en cada petición

La autenticación no es solo una comprobación de firma. Antes de ejecutar un manejador, la API verifica la firma y la caducidad del token, confirma que la sesión no ha sido revocada (tanto por la versión de token de todo el usuario como por el identificador individual de la sesión) y carga la pertenencia de quien llama a la organización que indica el token. Una pertenencia desactivada devuelve un 403. Cuando la organización está respaldada por un proveedor de identidad externo, la situación actual de quien llama se vuelve a comprobar en esa petición con el proveedor, en lugar de leerla del token; así, un acceso retirado en origen deja de funcionar de inmediato y no en la siguiente caducidad.

Credenciales que no son tokens de sesión

Existen otras dos credenciales de portador, y ninguna es una clave de uso general. Un token de dispositivo de voz se acepta solo en las rutas de llamadas de voz; una credencial de acceso de agente se acepta solo en el endpoint MCP. Presentarlas en cualquier otro lugar se rechaza con un 403 que nombra la credencial, y nunca se tratan como una sesión más débil. Consulta MCP y herramientas conectadas para saber cómo se emite la credencial del agente.

El envoltorio de respuesta

Toda respuesta correcta es un objeto con una clave data. No hay un array desnudo ni un payload en la raíz, así que un cliente nunca tiene que distinguir por la forma:

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

Los endpoints de listas añaden un objeto meta junto a esa clave. No se añade nada más en el nivel superior.

Los errores usan también una única forma, sea cual sea su origen:

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

code es la parte legible por máquina y es lo que debes usar para decidir; message está pensado para una persona. field y details aparecen cuando el error es lo bastante concreto como para localizarlo. Un error no controlado del servidor es siempre 500 INTERNAL_ERROR con un mensaje fijo: el texto original del error se registra, nunca se devuelve, porque podría haber recogido datos de la petición por el camino.

Códigos de error con los que te encontrarás primero

EstadoCódigoSignificado
400VALIDATION_ERROREl cuerpo o la consulta no coinciden con el esquema.
401AUTH_REQUIREDNo hay token de portador, o la cabecera está mal formada.
401TOKEN_EXPIREDEl token de acceso ha caducado. Renueva y vuelve a intentarlo.
401TOKEN_REVOKEDLa sesión se cerró, o se expulsó a todas las sesiones.
403ACCOUNT_DEACTIVATEDLa pertenencia a esta organización ha sido desactivada.
404NOT_FOUNDNo existe esa ruta, o no hay ningún registro visible para quien llama.
429RATE_LIMITEDUn cupo está agotado. Lee Retry-After.
500INTERNAL_ERRORUn error no controlado del servidor, deliberadamente opaco.

Paginación

Los endpoints de listas paginan con un cursor opaco de clave, no con un desplazamiento: se insertan filas mientras alguien lee, y con un desplazamiento la segunda página repite en silencio un registro que ya mostró la primera.

Se aceptan tres parámetros de consulta: cursor, limit y direction (forward o backward). La página por defecto tiene 25 elementos y el máximo es 100; un valor mayor se ajusta a ese límite en lugar de rechazarse. Los endpoints que además ordenan o filtran añaden sort, order y una búsqueda libre q.

El meta de la página te indica dónde estás:

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

nextCursor y prevCursor son ambos reales: Anterior y Siguiente son dos controles que una persona espera encontrar. Son opacos: el servidor codifica en ellos su propia clave de orden, y un cliente que los analice o los construya se romperá en cuanto una lista cambie su orden. total aparece en la mayoría de las listas y se omite cuando contar no tiene sentido, como en los resultados de búsqueda ordenados por relevancia.

Límites de frecuencia

Los límites son por ventana fija y se aplican antes incluso de leer el cuerpo de la petición. Una petición rechazada es un 429 con el código RATE_LIMITED y una cabecera Retry-After en segundos.

La mayoría de los cupos se basan en la IP del cliente; los sensibles a las credenciales —inicio de sesión, renovación, reconfirmación de contraseña, escrituras de credenciales MCP— mantienen además un contador por cuenta y rechazan cuando cualquiera de los dos se agota, de modo que tanto una dirección que prueba muchas cuentas como una cuenta atacada desde muchas direcciones chocan con un muro. El inicio de sesión admite por defecto 10 intentos por IP y 5 por dirección de correo cada diez minutos; publicar un mensaje admite 60 por minuto, y las escrituras bajo /api/agents, también 60 por minuto. Cada umbral es configurable por despliegue, así que trata estos valores como los predeterminados del producto y no como un contrato. Las comprobaciones de estado y los flujos en tiempo real están exentos.

Tiempo real

Consultar una lista repetidamente no es la forma correcta de seguir una conversación. Existen tres endpoints de streaming para ello:

  • GET /api/events/stream: eventos enviados por el servidor (SSE) de todo lo que la persona conectada puede ver.
  • GET /api/threads/:threadId/stream: eventos enviados por el servidor para un solo hilo. Respeta Last-Event-ID, así que una reconexión reanuda en lugar de empezar de nuevo.
  • GET /api/activity: un WebSocket que transporta el feed de actividad.

Los navegadores no pueden fijar cabeceras en el handshake de un WebSocket, así que la ruta WebSocket también acepta el token de acceso como parámetro de consulta ?token=. Es el único lugar donde un token debe aparecer en una URL; todas las demás rutas lo reciben en la cabecera.

Las familias de recursos

Hay muy por encima de mil endpoints, demasiados para listarlos. Se agrupan en familias, y una vez que conoces el prefijo de una familia, las rutas individuales son previsibles: una colección en el prefijo, un registro debajo y las acciones con nombre como subrutas POST.

FamiliaPrefijoQué incluye
Autenticación/api/authInicio de sesión, renovación, cierre de sesión, la identidad actual y las sesiones propias de una persona.
Canales e hilos/api/channels, /api/threadsEl modelo de conversación: los canales contienen hilos, los hilos contienen mensajes, y las respuestas se anidan bajo un mensaje raíz.
Agentes/api/agentsDefiniciones de agentes, sus vinculaciones con canales, conversaciones, tareas pendientes y estado.
Proyectos y tableros/api/projects, /api/tasks, /api/iterationsSeguimiento del trabajo: tableros y columnas dentro de un proyecto, tickets en ellos e iteraciones sobre esos tableros.
Conocimiento/api/knowledge-baseEspacios de páginas y archivos versionados, con comentarios, enlaces inversos y búsqueda.
Correo/api/mail, /api/mailbox-connectionsBuzones conectados, buzones de agentes y borradores que un agente ha redactado para que una persona los apruebe.
Aprobaciones/api/approvalsLa cola de acciones que un agente no puede ejecutar por su cuenta, y la única llamada que las resuelve.
Auditoría/api/audit-logEl registro de auditoría de la organización, encadenado mediante hash, con un endpoint de verificación. Propietarios y administradores.
Coste y uso/api/ledger, /api/billingContabilidad de tokens y costes, presupuestos y tiempos de ejecución; por separado, créditos y recargas. Propietarios y administradores.
Organización y equipos/api/organizations, /api/teams, /api/usersAdministración de la organización, de sus equipos y de sus miembros, incluidas las invitaciones.
Disparadores/api/triggersProgramaciones y webhooks que inician trabajo de agentes, con su historial de activaciones.
Llamadas y voz/api/calls, /api/voiceLlamadas dentro de un canal, y la sesión, la transcripción y los tokens de dispositivo del asistente de voz.
Ejecutores/api/executors, /api/execution-environment-templatesLas máquinas en las que trabajan los agentes, sus entornos y el canal de emparejamiento del daemon.
Ejecuciones/api/runsEjecuciones de agentes en curso: cancelar, continuar, reiniciar y vincular a un ejecutor.
Subidas/api/uploads, /api/attachmentsSube primero un archivo y luego vincula el identificador devuelto a un mensaje.
Búsqueda/api/messages/searchBúsqueda de mensajes, en toda la organización o limitada a un canal.
Secretos y ajustes/api/secrets, /api/settings/scopedAlmacenamiento de credenciales de solo escritura con permisos explícitos, y valores de configuración en capas.
Herramientas conectadas/api/mcpServidores MCP a los que esta instancia llama. Consulta MCP y herramientas conectadas.
Estado/api/healthVivacidad y disponibilidad, ambas públicas. /api/health/ready comprueba además la base de datos.

Los permisos se aplican por ruta, no por familia. En general, el registro de auditoría, el registro de costes, las reglas de política y el plano de control de inferencia son superficies para propietarios y administradores de la organización, mientras que las conversaciones, los proyectos, el conocimiento y las aprobaciones están abiertos a cualquier miembro y se acotan según lo que ese miembro puede ver realmente.

Ejemplos prácticos

Iniciar sesión y guardar la cookie de renovación

En una instancia en modo local, con contraseña. El token de acceso llega en el cuerpo; el token de renovación acaba en el almacén 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
      }
    }
  }
}

Cuando el token caduque, cambia la cookie por uno nuevo:

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

La respuesta tiene la misma forma { token, me }, y el almacén de cookies ya guarda el token de renovación rotado.

Listar los canales que puedes ver

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

Añade ?teamId=… para limitarlo a un equipo, o ?includeArchived=true para incluir los canales archivados. El defaultThreadId es el hilo al que publicarás a continuación.

Publicar un mensaje

Un mensaje necesita algún content o al menos un adjunto subido. Envía una cabecera Idempotency-Key y una petición reintentada se resolverá en el mensaje que creó el primer intento, en lugar de publicar una segunda copia.

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 mensaje nuevo devuelve 201:

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

Una repetición con la misma clave de idempotencia responde 200 con el mensaje original y un array pendingAgentInvites vacío, porque el primer intento ya se encargó de los avisos y del envío que describe ese array.

Hay dos rechazos de esta ruta que conviene gestionar de forma explícita. El contenido que supera el límite de longitud del chat devuelve 413 MESSAGE_TOO_LARGE, y la respuesta correcta es subir el texto como archivo. El contenido que parece una credencial devuelve 422 SECRET_INTERCEPTED: el servidor lo rechaza antes de guardarlo, enviarlo o registrarlo, y la credencial debe ir en /api/secrets.