nessie

API

A API HTTP que suporta todos os clientes do Nessie e a forma de se autenticar nela.

O que é a API

O Nessie tem uma única API HTTP. A aplicação de administração Web, os clientes para macOS e iOS e o daemon do executor remoto são todos clientes normais dela, e nada do que podem fazer é acessível de outra forma. É uma API JSON sobre um servidor Fastify, não é GraphQL, e não tem um subconjunto separado «público»: a superfície que os scripts próprios chamam é a mesma que o produto chama.

Não há nenhum segmento de versão no caminho. Todas as rotas estão em /api, exceto o endpoint MCP de entrada em POST /mcp e alguns documentos de descoberta em /.well-known/. A compatibilidade é assegurada por só se acrescentarem campos, pelo que um cliente que ignora o que não reconhece continua a funcionar.

URL base

O Nessie pode ser alojado por quem o utiliza, por isso o URL base é a origem onde a implementação serve a API. O operador define-o como NESSIE_API_PUBLIC_URL, e é esse mesmo valor que a API usa quando tem de gerar um URL absoluto próprio. Na instância que executamos, é https://api.nessie.works; uma instância de desenvolvimento iniciada com pnpm dev responde em http://localhost:5454.

A aplicação de administração Web tem uma origem diferente da API, por isso os clientes de navegador ficam sujeitos a uma lista de origens permitidas em CORS (NESSIE_CORS_ORIGINS). Os pedidos feitos a partir de um script ou de um servidor não têm origem a verificar e não são afetados.

Autenticação

Todos os pedidos fora de um pequeno conjunto de rotas deliberadamente públicas levam um token de acesso no cabeçalho padrão:

Authorization: Bearer <token>

O token é um JWT assinado, emitido pela API. Tem vida curta — trinta minutos por predefinição (NESSIE_AUTH_TOKEN_TTL) — e é a única credencial que um cliente comum apresenta. O Nessie não tem chaves de API pessoais de longa duração. Se pretender que um script chame a API, ele inicia sessão da mesma forma que o navegador de uma pessoa e renova o token à medida que avança.

Obter um token

POST /api/auth/session cria uma sessão. Responde com o token de acesso e a identidade de quem chama, e define o token de renovação como um cookie separado, apenas HTTP.

A forma de comprovar a identidade depende de como a instância está implementada. Uma instância em modo local aceita um endereço de email e uma palavra-passe. Numa instância hosted ou selfHosted, o início de sessão por palavra-passe é recusado de imediato com 403 PASSWORD_AUTH_DISABLED, e a única entrada é o fornecedor de identidade configurado, para que a conta nunca tenha uma segunda identidade, mais fraca, ao lado dele.

Iniciar sessão através de um fornecedor de identidade

O início de sessão único (SSO) é uma troca de código de autorização OAuth com PKCE, em três passos. O fornecedor de identidade do Nessie é a UnlikeOtherAI, que também é quem gere a estrutura de organizações e equipas que uma instância espelha; a troca abaixo tem a mesma forma para qualquer fornecedor configurado.

  1. GET /api/auth/providers lista os fornecedores que esta instância aceita, cada um com um providerId, um rótulo e a indicação de a página de início de sessão dever redirecionar automaticamente para ele.
  2. GET /api/auth/providers/:providerId/authorize, com codeChallenge, redirectUri e state como parâmetros de consulta, responde com { data: { authorizeUrl } }. A pessoa é então enviada para lá.
  3. O fornecedor devolve a pessoa ao redirectUri com um código. Envie-o de volta por POST /api/auth/session com { providerId, code, codeVerifier, redirectUri } — os quatro campos são obrigatórios em conjunto, e um conjunto incompleto dá 400 EXTERNAL_AUTH_INCOMPLETE.

Renovar uma sessão

O token de renovação nunca aparece num corpo de resposta. É definido como cookie apenas HTTP com o nome nessie_refresh, limitado ao caminho /api/auth, com uma validade de trinta dias por predefinição (NESSIE_AUTH_REFRESH_TOKEN_TTL). Chamar POST /api/auth/refresh com esse cookie consome-o, emite um novo token de acesso e define um novo cookie no seu lugar — os tokens são substituídos a cada renovação, pelo que um token roubado só serve até o cliente legítimo renovar de novo.

Um navegador com sessão iniciada em várias contas guarda um cookie desses por início de sessão, chamado nessie_refresh_<slot>, e indica a qual deles se refere cada pedido através do cabeçalho x-nessie-account-slot. Um cliente que não envia esse cabeçalho refere-se ao nessie_refresh, que é tudo o que um cliente de linha de comandos precisa.

Um cliente de linha de comandos precisa, por isso, de um ficheiro de cookies (cookie jar). Com curl, isso é --cookie-jar no pedido de início de sessão e --cookie na renovação, como no exemplo trabalhado abaixo.

O que é verificado em cada pedido

A autenticação não é apenas uma verificação de assinatura. Antes de um manipulador correr, a API verifica a assinatura e a validade do token, confirma que a sessão não foi revogada (tanto pela versão de token de todo o utilizador como pelo identificador individual da sessão) e carrega a pertença de quem chama à organização que o token indica. Uma pertença desativada resulta num 403. Quando a organização é apoiada por um fornecedor de identidade externo, a situação atual de quem chama é novamente verificada junto desse fornecedor no próprio pedido, em vez de ser lida do token, pelo que um acesso retirado a montante deixa de funcionar de imediato e não só na expiração seguinte.

Credenciais que não são tokens de sessão

Existem outras duas credenciais de portador, e nenhuma é uma chave de uso geral. Uma credencial de dispositivo de voz só é aceite nas rotas de chamadas de voz; uma credencial de acesso de agente só é aceite no endpoint MCP. Apresentar qualquer uma delas noutro lugar é recusado com um 403 que identifica a credencial, e não é repetido como uma sessão mais fraca — veja MCP e ferramentas ligadas para saber como é emitida a credencial do agente.

O formato das respostas

Todas as respostas bem-sucedidas são um objeto com uma chave data. Não há nenhum array simples nem nenhuma carga útil na raiz, pelo que um cliente nunca tem de distinguir pela forma:

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

As listas acrescentam um objeto meta ao lado dele. Nunca se acrescenta mais nada ao nível de topo.

As falhas também usam uma única forma, seja qual for a causa:

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

code é a parte legível por máquina e é nela que deve basear a decisão; message destina-se a uma pessoa. field e details aparecem quando a falha é suficientemente específica para ser localizada. Um erro não tratado do servidor é sempre 500 INTERNAL_ERROR com uma mensagem fixa — o texto original do erro é registado, nunca devolvido, porque pode ter apanhado dados do pedido pelo caminho.

Códigos de erro que vai encontrar primeiro

EstadoCódigoSignificado
400VALIDATION_ERRORO corpo ou a consulta não correspondem ao esquema.
401AUTH_REQUIREDSem token de portador, ou o cabeçalho está mal formado.
401TOKEN_EXPIREDO token de acesso passou da validade. Renove-o e tente de novo.
401TOKEN_REVOKEDA sessão foi terminada, ou todas as sessões foram forçadas a terminar.
403ACCOUNT_DEACTIVATEDA pertença a esta organização foi desativada.
404NOT_FOUNDRota inexistente, ou registo inexistente visível para quem chama.
429RATE_LIMITEDUm limite está esgotado. Leia o Retry-After.
500INTERNAL_ERRORUm erro não tratado do servidor, deliberadamente genérico.

Paginação

As listas são paginadas com um cursor de chave opaco, e não com um deslocamento — os registos são inseridos enquanto alguém lê, e com um deslocamento a segunda página repete em silêncio um registo que a primeira já mostrou.

São aceites três parâmetros de consulta: cursor, limit e direction (forward ou backward). A página predefinida tem 25 itens e o máximo é 100; um valor superior é limitado a esse máximo, e não rejeitado. Os endpoints que também ordenam ou filtram acrescentam sort, order e uma pesquisa de texto livre q.

O meta da página indica onde se está:

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

nextCursor e prevCursor são ambos reais: Anterior e Seguinte são controlos que uma pessoa espera. São opacos — o servidor codifica nelas a sua própria chave de ordenação, e um cliente que as analise ou construa deixará de funcionar no momento em que uma lista mude a sua ordem. total está presente na maioria das listas e é omitido onde a contagem não faz sentido, como nos resultados de pesquisa ordenados por relevância.

Limites de pedidos

Os limites aplicam-se por janela fixa e são impostos antes de o corpo do pedido sequer ser lido. Um pedido rejeitado é 429 com o código RATE_LIMITED e um cabeçalho Retry-After em segundos.

A maioria dos limites tem como chave o endereço IP do cliente; os sensíveis a credenciais — início de sessão, renovação, nova comprovação da palavra-passe e escritas de credenciais MCP — mantêm também um contador por conta e rejeitam quando qualquer um dos dois é atingido, de modo que um endereço que tenta muitas contas e uma conta atacada a partir de muitos endereços esbarram ambos num limite. O início de sessão tem por predefinição 10 tentativas por IP e 5 por endereço de email em dez minutos; publicar uma mensagem tem por predefinição 60 por minuto; as escritas em /api/agents têm 60 por minuto. Cada limite é configurável por implementação, por isso trate estes valores como as predefinições enviadas, e não como um contrato. As verificações de estado e os streams em tempo real estão isentos.

Tempo real

Ir consultando uma lista de cada vez é a forma errada de acompanhar uma conversa. Existem três endpoints de streaming para isso:

  • GET /api/events/stream — eventos enviados pelo servidor (server-sent events) de tudo o que a pessoa com sessão iniciada pode ver.
  • GET /api/threads/:threadId/stream — eventos enviados pelo servidor de uma só thread. Respeita Last-Event-ID, pelo que uma reconexão retoma em vez de recomeçar.
  • GET /api/activity — um WebSocket que transporta o feed de atividade.

Os navegadores não podem definir cabeçalhos no handshake de um WebSocket, por isso a rota WebSocket aceita também o token de acesso como parâmetro de consulta ?token=. É o único sítio onde um token pertence a um URL; todas as outras rotas o recebem no cabeçalho.

As famílias de recursos

Há mais de mil endpoints, um número demasiado grande para listar. Estão agrupados em famílias e, uma vez conhecido o prefixo de uma família, as rotas individuais são previsíveis — uma coleção no prefixo, um registo por baixo dele e ações com nome como subcaminhos POST.

FamíliaPrefixoO que abrange
Autenticação/api/authInício de sessão, renovação, fim de sessão, identidade atual e as sessões da própria pessoa.
Canais e threads/api/channels, /api/threadsO modelo de conversa: os canais contêm threads, as threads contêm mensagens, e as respostas ficam aninhadas sob a mensagem inicial.
Agentes/api/agentsDefinições de agentes, respetivas associações a canais, conversas, tarefas a fazer e estado.
Projetos e quadros/api/projects, /api/tasks, /api/iterationsAcompanhamento de trabalho: quadros e colunas por baixo de um projeto, tarefas nesses quadros e iterações sobre estes.
Conhecimento/api/knowledge-baseEspaços de páginas e ficheiros com versões, com comentários, ligações de retorno e pesquisa.
Correio/api/mail, /api/mailbox-connectionsCaixas de correio ligadas, caixas de correio de agentes e rascunhos que um agente escreveu para uma pessoa aprovar.
Aprovações/api/approvalsA fila de ações que um agente não pode tomar por iniciativa própria, e a única chamada que as resolve.
Auditoria/api/audit-logO registo de auditoria da organização, encadeado por hash, com um endpoint de verificação. Proprietários e administradores.
Custos e utilização/api/ledger, /api/billingContabilização de tokens e custos, orçamentos e tempos de execução; em separado, créditos e recargas. Proprietários e administradores.
Organização e equipas/api/organizations, /api/teams, /api/usersAdministração do tenant, das equipas e das pertenças, incluindo convites.
Gatilhos/api/triggersAgendamentos e webhooks que iniciam trabalho de agentes, com o respetivo histórico de disparos.
Chamadas e voz/api/calls, /api/voiceChamadas dentro de um canal, e a sessão, a transcrição e os tokens de dispositivo do assistente de voz.
Executores/api/executors, /api/execution-environment-templatesAs máquinas em que os agentes trabalham, os respetivos ambientes e o canal de associação do daemon.
Execuções/api/runsExecuções de agentes em curso: cancelar, continuar, reiniciar e associar a um executor.
Carregamentos/api/uploads, /api/attachmentsCarregar primeiro um ficheiro e depois associar o identificador devolvido a uma mensagem.
Pesquisa/api/messages/searchPesquisa de mensagens, em toda a organização ou limitada a um canal.
Segredos e definições/api/secrets, /api/settings/scopedArmazenamento de credenciais apenas de escrita, com concessões explícitas, e valores de configuração em camadas.
Ferramentas ligadas/api/mcpServidores MCP que esta instância chama. Veja MCP e ferramentas ligadas.
Estado/api/healthVerificações de funcionamento e de prontidão, ambas públicas. /api/health/ready verifica também a base de dados.

Os direitos são aplicados por rota, e não por família. Em termos gerais: o registo de auditoria, o registo de custos, as regras de política e o plano de controlo de inferência são superfícies para os proprietários e administradores da organização, enquanto as conversas, os projetos, o conhecimento e as aprovações estão abertos a qualquer membro e restringidos pelo que esse membro consegue efetivamente ver.

Exemplos trabalhados

Iniciar sessão e guardar o cookie de renovação

Numa instância em modo local, com palavra-passe. O token de acesso vem no corpo da resposta; o token de renovação fica no ficheiro 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
      }
    }
  }
}

Quando o token expirar, troque o cookie por um novo:

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

A resposta tem a mesma forma { token, me }, e o ficheiro de cookies passa a conter o token de renovação substituído.

Listar os canais visíveis

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

Acrescente ?teamId=… para limitar a uma equipa, ou ?includeArchived=true para incluir os canais arquivados. O defaultThreadId é o destino da próxima publicação.

Publicar uma mensagem

Uma mensagem precisa de algum content ou de pelo menos um anexo carregado. Envie uma Idempotency-Key e um pedido repetido resolve-se na mensagem que a primeira tentativa criou, em vez de publicar uma segunda cópia.

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

Uma mensagem nova devolve 201:

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

Uma repetição com a mesma chave de idempotência responde com 200, com a mensagem original e um pendingAgentInvites vazio, porque a primeira tentativa já fez os alertas e o envio que o array descreve.

Duas recusas nesta rota merecem tratamento explícito. Um conteúdo acima do limite de tamanho das mensagens dá 413 MESSAGE_TOO_LARGE, e a resposta certa é carregar o texto como ficheiro. Um conteúdo que tem a estrutura de uma credencial dá 422 SECRET_INTERCEPTED — o servidor recusa-o antes de ser guardado, encaminhado ou registado, e a credencial pertence a /api/secrets.