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.
GET /api/auth/providerslista os fornecedores que esta instância aceita, cada um com umproviderId, um rótulo e a indicação de a página de início de sessão dever redirecionar automaticamente para ele.GET /api/auth/providers/:providerId/authorize, comcodeChallenge,redirectUriestatecomo parâmetros de consulta, responde com{ data: { authorizeUrl } }. A pessoa é então enviada para lá.- O fornecedor devolve a pessoa ao
redirectUricom um código. Envie-o de volta porPOST /api/auth/sessioncom{ 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
| Estado | Código | Significado |
|---|---|---|
| 400 | VALIDATION_ERROR | O corpo ou a consulta não correspondem ao esquema. |
| 401 | AUTH_REQUIRED | Sem token de portador, ou o cabeçalho está mal formado. |
| 401 | TOKEN_EXPIRED | O token de acesso passou da validade. Renove-o e tente de novo. |
| 401 | TOKEN_REVOKED | A sessão foi terminada, ou todas as sessões foram forçadas a terminar. |
| 403 | ACCOUNT_DEACTIVATED | A pertença a esta organização foi desativada. |
| 404 | NOT_FOUND | Rota inexistente, ou registo inexistente visível para quem chama. |
| 429 | RATE_LIMITED | Um limite está esgotado. Leia o Retry-After. |
| 500 | INTERNAL_ERROR | Um 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. RespeitaLast-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ília | Prefixo | O que abrange |
|---|---|---|
| Autenticação | /api/auth | Iní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/threads | O modelo de conversa: os canais contêm threads, as threads contêm mensagens, e as respostas ficam aninhadas sob a mensagem inicial. |
| Agentes | /api/agents | Definições de agentes, respetivas associações a canais, conversas, tarefas a fazer e estado. |
| Projetos e quadros | /api/projects, /api/tasks, /api/iterations | Acompanhamento de trabalho: quadros e colunas por baixo de um projeto, tarefas nesses quadros e iterações sobre estes. |
| Conhecimento | /api/knowledge-base | Espaços de páginas e ficheiros com versões, com comentários, ligações de retorno e pesquisa. |
| Correio | /api/mail, /api/mailbox-connections | Caixas de correio ligadas, caixas de correio de agentes e rascunhos que um agente escreveu para uma pessoa aprovar. |
| Aprovações | /api/approvals | A 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-log | O 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/billing | Contabilizaçã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/users | Administração do tenant, das equipas e das pertenças, incluindo convites. |
| Gatilhos | /api/triggers | Agendamentos e webhooks que iniciam trabalho de agentes, com o respetivo histórico de disparos. |
| Chamadas e voz | /api/calls, /api/voice | Chamadas 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-templates | As máquinas em que os agentes trabalham, os respetivos ambientes e o canal de associação do daemon. |
| Execuções | /api/runs | Execuções de agentes em curso: cancelar, continuar, reiniciar e associar a um executor. |
| Carregamentos | /api/uploads, /api/attachments | Carregar primeiro um ficheiro e depois associar o identificador devolvido a uma mensagem. |
| Pesquisa | /api/messages/search | Pesquisa de mensagens, em toda a organização ou limitada a um canal. |
| Segredos e definições | /api/secrets, /api/settings/scoped | Armazenamento de credenciais apenas de escrita, com concessões explícitas, e valores de configuração em camadas. |
| Ferramentas ligadas | /api/mcp | Servidores MCP que esta instância chama. Veja MCP e ferramentas ligadas. |
| Estado | /api/health | Verificaçõ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.cookiesA 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.