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, которая также ведёт структуру организаций и команд, отражаемую экземпляром; обмен ниже имеет ту же форму для любого настроенного поставщика.
GET /api/auth/providersперечисляет поставщиков, которых принимает этот экземпляр; у каждого естьproviderId, название и признак, нужно ли странице входа перенаправлять на него автоматически.GET /api/auth/providers/:providerId/authorizeс параметрами запросаcodeChallenge,redirectUriиstateотвечает{ data: { authorizeUrl } }. Отправьте пользователя туда.- Поставщик возвращает пользователя на ваш
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 с фиксированным сообщением: текст ошибки записывается в журнал, но не возвращается, потому что он мог захватить данные запроса.
Коды ошибок, с которыми вы столкнётесь первыми
| Статус | Код | Значение |
|---|---|---|
| 400 | VALIDATION_ERROR | Тело или параметры запроса не соответствуют схеме. |
| 401 | AUTH_REQUIRED | Нет bearer-токена, или заголовок повреждён. |
| 401 | TOKEN_EXPIRED | Срок действия токена доступа истёк. Обновите его и повторите запрос. |
| 401 | TOKEN_REVOKED | Сессия завершена или все сессии принудительно закрыты. |
| 403 | ACCOUNT_DEACTIVATED | Членство в этой организации деактивировано. |
| 404 | NOT_FOUND | Нет такого маршрута или записи, видимой этому вызывающему. |
| 429 | RATE_LIMITED | Лимит исчерпан. Читайте Retry-After. |
| 500 | INTERNAL_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/mcp | MCP-серверы, к которым обращается эта установка. См. 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.