nessie

API

HTTP API, на котором работает каждый клиент Nessie, и как проходить аутентификацию в нём.

Что представляет собой API

У Nessie один HTTP API. Веб-приложение администрирования, клиенты для macOS и iOS и удалённый демон исполнителя — обычные клиенты этого API, и ничего из того, что они умеют, нельзя получить другим путём. Это JSON API поверх сервера Fastify, а не GraphQL, и у него нет отдельного «публичного» подмножества: то, что вызывают ваши скрипты, — это то же, что вызывает продукт.

В пути нет сегмента версии. Все маршруты находятся под /api, кроме входящего эндпоинта MCP POST /mcp и нескольких документов обнаружения в /.well-known/. Совместимость обеспечивается тем, что поля только добавляются, поэтому клиент, который игнорирует незнакомые ему поля, продолжает работать.

Базовый URL

Nessie можно развернуть на собственных серверах, поэтому базовый URL — это адрес, по которому ваше развёртывание отдаёт API. Оператор задаёт его в NESSIE_API_PUBLIC_URL, и то же значение API использует, когда ему нужно сформировать собственный абсолютный URL. На нашем экземпляре это https://api.nessie.works; экземпляр для разработки, запущенный командой pnpm dev, отвечает на http://localhost:5454.

Веб-приложение администрирования работает на другом origin, чем API, поэтому браузерные клиенты подчиняются списку разрешённых источников CORS (NESSIE_CORS_ORIGINS). Запросы из скрипта или сервера не имеют origin для проверки и на них это не влияет.

Аутентификация

Каждый запрос, кроме небольшого набора намеренно открытых маршрутов, несёт токен доступа в стандартном заголовке:

Authorization: Bearer <token>

Токен — это подписанный JWT, выпущенный API. Он короткоживущий: по умолчанию тридцать минут (NESSIE_AUTH_TOKEN_TTL), и это единственное, что обычный клиент предъявляет при каждом запросе. Долгоживущих персональных API-ключей в Nessie нет. Если скрипту нужно обращаться к API, он входит так же, как браузер пользователя, и продлевает свой токен по ходу работы.

Получение токена

POST /api/auth/session создаёт сессию. Ответ содержит токен доступа и личность вызывающего, а токен обновления устанавливается отдельным cookie HTTP-only.

Способ подтвердить личность зависит от того, как развёрнут экземпляр. Экземпляр в режиме local принимает email и пароль. На экземплярах hosted или selfHosted вход по паролю полностью отклоняется с 403 PASSWORD_AUTH_DISABLED, и войти можно только через настроенного поставщика идентификации. Поэтому у учётной записи никогда не появляется второй, более слабый путь входа рядом с ним.

Вход через поставщика идентификации

Единый вход — это обмен кода авторизации OAuth с PKCE в три шага. Поставщик идентификации Nessie — UnlikeOtherAI, которая также ведёт структуру организаций и команд, отражаемую экземпляром; обмен ниже имеет ту же форму для любого настроенного поставщика.

  1. GET /api/auth/providers перечисляет поставщиков, которых принимает этот экземпляр; у каждого есть providerId, название и признак, нужно ли странице входа перенаправлять на него автоматически.
  2. GET /api/auth/providers/:providerId/authorize с параметрами запроса codeChallenge, redirectUri и state отвечает { data: { authorizeUrl } }. Отправьте пользователя туда.
  3. Поставщик возвращает пользователя на ваш redirectUri с кодом. Отправьте его обратно через POST /api/auth/session с { providerId, code, codeVerifier, redirectUri }: все четыре поля обязательны вместе, а неполный набор даёт 400 EXTERNAL_AUTH_INCOMPLETE.

Продление сессии

Токен обновления никогда не попадает в тело ответа. Он устанавливается как cookie HTTP-only с именем nessie_refresh, областью пути /api/auth и сроком жизни по умолчанию тридцать дней (NESSIE_AUTH_REFRESH_TOKEN_TTL). Вызов POST /api/auth/refresh с этим cookie использует его, выдаёт новый токен доступа и устанавливает новый cookie на его место. Токены ротируются, поэтому украденный токен действует только до следующего обновления настоящим клиентом.

Браузер, вошедший в несколько учётных записей, хранит по одному такому cookie на каждый вход, с именем nessie_refresh_<slot>, и заголовком x-nessie-account-slot указывает, к какому из них относится запрос. Запрос без такого заголовка относится к nessie_refresh; для клиента командной строки этого достаточно.

Поэтому клиенту командной строки нужен cookie-jar. В curl это --cookie-jar при вызове входа и --cookie при обновлении, как в примере ниже.

Что проверяется при каждом запросе

Аутентификация — не только проверка подписи. До выполнения обработчика API проверяет подпись и срок действия токена, убеждается, что сессия не отозвана (и по версии токена для всего пользователя, и по идентификатору отдельной сессии), и загружает членство вызывающего в организации, которую назван токен. Деактивированное членство даёт 403. Если организация опирается на внешнего поставщика идентификации, текущее состояние вызывающего проверяется у этого поставщика при каждом запросе, а не берётся из токена. Поэтому доступ, отозванный там, перестаёт работать сразу, а не при следующем истечении срока.

Учётные данные, которые не являются токенами сессии

Существуют ещё два вида bearer-учётных данных, и ни один из них не является универсальным ключом. Учётные данные голосового устройства принимаются только на маршрутах голосовых звонков, а учётные данные доступа агента — только на эндпоинте MCP. Предъявление любого из них в другом месте отклоняется с 403, в ответе назван вид учётных данных; повторной попытки как более слабой сессии не будет. О том, как выдаются учётные данные агента, см. в разделе MCP и подключённые инструменты.

Структура ответа

Каждый успешный ответ — объект с ключом data. Голого массива и полезной нагрузки в корне нет, поэтому клиенту никогда не нужно определять форму ответа по его виду:

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

Списочные эндпоинты добавляют рядом объект meta. На верхнем уровне больше ничего не добавляется.

Ошибки имеют одну форму независимо от того, что их вызвало:

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

code — машиночитаемая часть, по ней и нужно ветвить логику; message — для человека. field и details присутствуют, когда ошибку можно точно привязать к месту. Необработанная ошибка сервера всегда возвращается как 500 INTERNAL_ERROR с фиксированным сообщением: текст ошибки записывается в журнал, но не возвращается, потому что он мог захватить данные запроса.

Коды ошибок, с которыми вы столкнётесь первыми

СтатусКодЗначение
400VALIDATION_ERRORТело или параметры запроса не соответствуют схеме.
401AUTH_REQUIREDНет bearer-токена, или заголовок повреждён.
401TOKEN_EXPIREDСрок действия токена доступа истёк. Обновите его и повторите запрос.
401TOKEN_REVOKEDСессия завершена или все сессии принудительно закрыты.
403ACCOUNT_DEACTIVATEDЧленство в этой организации деактивировано.
404NOT_FOUNDНет такого маршрута или записи, видимой этому вызывающему.
429RATE_LIMITEDЛимит исчерпан. Читайте Retry-After.
500INTERNAL_ERRORНеобработанная ошибка сервера, намеренно без подробностей.

Разбиение на страницы

Списочные эндпоинты разбиваются на страницы с непрозрачным курсором keyset, а не смещением: строки добавляются, пока кто-то читает, и при смещении вторая страница молча повторяет запись, которую уже показала первая.

Принимаются три параметра запроса: cursor, limit и direction (forward или backward). Страница по умолчанию содержит 25 записей, максимум — 100; большее значение обрезается до максимума, а не отклоняется. Эндпоинты, которые ещё и сортируют или фильтруют, добавляют sort, order и свободный текстовый параметр q.

В meta страницы видно, где вы находитесь:

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

nextCursor и prevCursor заполнены оба: пользователь ожидает обе кнопки, «Назад» и «Далее». Курсоры непрозрачны: сервер кодирует в них собственный ключ сортировки, и клиент, который разбирает или составляет курсор, сломается, как только список изменит порядок. total есть в большинстве списков и опускается там, где подсчёт не имеет смысла, например для ранжированных результатов поиска.

Лимиты запросов

Лимиты действуют в фиксированных окнах и применяются до того, как прочитано тело запроса. Отклонённый запрос получает 429 с кодом RATE_LIMITED и заголовком Retry-After в секундах.

Большинство лимитов привязано к IP-адресу клиента. Чувствительные к учётным данным — вход, обновление, повторное подтверждение пароля и записи учётных данных MCP — дополнительно ведут счётчик по учётной записи и отклоняют запрос, если сработал любой из них. Поэтому и один адрес, перебирающий много учётных записей, и одна учётная запись, атакуемая с многих адресов, упираются в ограничение. По умолчанию вход допускает 10 попыток с одного IP и 5 на один адрес электронной почты за десять минут; отправка сообщения — 60 в минуту; записи под /api/agents — 60 в минуту. Каждый порог настраивается для установки, поэтому считайте эти значения стандартными по умолчанию, а не контрактом. Проверки состояния и потоки реального времени не ограничиваются.

Реальное время

Опрос списка — неподходящий способ следить за беседой. Вместо него есть три потоковых эндпоинта:

  • GET /api/events/stream — server-sent events для всего, что видит вошедший пользователь.
  • GET /api/threads/:threadId/stream — server-sent events для одной ветки. Он учитывает Last-Event-ID, поэтому переподключение продолжает поток, а не начинает его заново.
  • GET /api/activity — WebSocket с лентой активности.

Браузеры не могут задавать заголовки при рукопожатии WebSocket, поэтому маршрут WebSocket также принимает токен доступа в параметре запроса ?token=. Это единственное место, где токен может находиться в URL; все остальные маршруты принимают его в заголовке.

Семейства ресурсов

Конечных точек намного больше тысячи — перечислять их все бессмысленно. Они сгруппированы по семействам; зная префикс семейства, отдельные маршруты легко предсказать: коллекция по префиксу, запись под ней и именованные действия как подпути POST.

СемействоПрефиксЧто охватывает
Аутентификация/api/authВход, продление, выход, текущая личность и собственные сессии пользователя.
Каналы и ветки/api/channels, /api/threadsМодель бесед: каналы содержат ветки, ветки содержат сообщения, ответы вложены в корневое сообщение.
Агенты/api/agentsОпределения агентов, их привязки к каналам, беседы, задачи и статус.
Проекты и доски/api/projects, /api/tasks, /api/iterationsУчёт работы: доски и колонки внутри проекта, задачи на них и итерации над ними.
База знаний/api/knowledge-baseПространства версионированных страниц и файлов с комментариями, обратными ссылками и поиском.
Почта/api/mail, /api/mailbox-connectionsПодключённые почтовые ящики, почтовые ящики агентов и черновики, которые агент подготовил для одобрения человеком.
Согласования/api/approvalsОчередь действий, которые агент не может совершить самостоятельно, и один вызов, который их разрешает.
Аудит/api/audit-logЖурнал аудита организации с цепочкой хешей и эндпоинтом проверки. Владельцы и администраторы.
Расходы и использование/api/ledger, /api/billingУчёт токенов и стоимости, бюджеты и время выполнения; отдельно — кредиты и пополнения. Владельцы и администраторы.
Организация и команды/api/organizations, /api/teams, /api/usersАдминистрирование организации, команд и участников, включая приглашения.
Триггеры/api/triggersРасписания и вебхуки, которые запускают работу агентов, с историей срабатываний.
Звонки и голос/api/calls, /api/voiceЗвонки внутри канала, а также сессия голосового помощника, расшифровка и токены устройств.
Исполнители/api/executors, /api/execution-environment-templatesКомпьютеры, на которых работают агенты, их окружения и канал подключения демона.
Запуски/api/runsЖивые запуски агентов: отмена, продолжение, перезапуск, привязка к исполнителю.
Загрузки/api/uploads, /api/attachmentsСначала загрузите файл, затем привяжите возвращённый идентификатор к сообщению.
Поиск/api/messages/searchПоиск по сообщениям во всей организации или в одном канале.
Секреты и настройки/api/secrets, /api/settings/scopedХранилище учётных данных только для записи с явными разрешениями и многоуровневые значения настроек.
Подключённые инструменты/api/mcpMCP-серверы, к которым обращается эта установка. См. MCP и подключённые инструменты.
Состояние/api/healthПроверки работоспособности и готовности, обе публичные. /api/health/ready также проверяет базу данных.

Права проверяются для каждого маршрута, а не для семейства. В общих чертах: журнал аудита, учёт затрат, правила политик и управление инференсом доступны владельцам и администраторам организации, а беседы, проекты, база знаний и согласования открыты любому участнику, но ограничены тем, что этот участник действительно видит.

Примеры

Вход и сохранение cookie обновления

На экземпляре в режиме local, по паролю. Токен доступа возвращается в теле ответа; токен обновления попадает в cookie-jar.

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

Когда токен истечёт, обменяйте cookie на новый:

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

Ответ имеет ту же форму { token, me }, а cookie-jar теперь хранит обновлённый токен обновления.

Список доступных каналов

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

Добавьте ?teamId=…, чтобы оставить одну команду, или ?includeArchived=true, чтобы включить архивные каналы. defaultThreadId — ветка, в которую уйдёт следующее сообщение.

Отправка сообщения

Сообщению нужно либо content, либо хотя бы одно загруженное вложение. Отправьте Idempotency-Key: повторный запрос вернёт сообщение, созданное первой попыткой, а не вторую копию.

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

Новое сообщение возвращается с кодом 201:

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

Повтор с тем же ключом идемпотентности отвечает 200 с исходным сообщением и пустым pendingAgentInvites, потому что первая попытка уже выполнила оповещения и отправку, которые описывает этот массив.

На этом маршруте стоит отдельно обработать два отказа. Содержимое длиннее лимита чата получает 413 MESSAGE_TOO_LARGE; правильный ответ — загрузить текст как файл. Содержимое, похожее по структуре на учётные данные, получает 422 SECRET_INTERCEPTED: сервер отклоняет его до сохранения, отправки или записи в журнал, а учётным данным место в /api/secrets.