nessie

API

HTTP API, на якому працює кожен клієнт Nessie, і як проходити автентифікацію в ньому.

Що таке API

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

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

Базова адреса

Nessie встановлюється на власному сервері, тож базова адреса — це та адреса (origin), за якою ваше розгортання обслуговує API. Оператор задає її як NESSIE_API_PUBLIC_URL, і те саме значення API використовує, коли має сформувати власну абсолютну адресу. На сервері, який ми запускаємо, це https://api.nessie.works; екземпляр для розробки, запущений командою pnpm dev, відповідає за адресою http://localhost:5454.

Веб-застосунок адміністратора розташований на іншому origin, ніж API, тому браузерні клієнти підпорядковуються списку дозволених джерел CORS (NESSIE_CORS_ORIGINS). Запити зі скрипта або сервера не мають джерела для перевірки, тож на них це не впливає.

Автентифікація

Кожен запит, окрім невеликого набору навмисно публічних маршрутів, передає токен доступу в стандартному заголовку:

Authorization: Bearer <token>

Токен — це підписаний JWT, який створює API. Він короткоживучий — за замовчуванням тридцять хвилин (NESSIE_AUTH_TOKEN_TTL) — і є єдиними обліковими даними, які звичайний викликач будь-коли пред’являє. У Nessie немає довготривалих персональних ключів API. Якщо скрипт має викликати API, він входить так само, як браузер людини, і оновлює токен у процесі роботи.

Отримання токена

POST /api/auth/session створює сеанс. Він повертає токен доступу та ідентичність викликача і встановлює токен поновлення в окремому cookie HTTP-only.

Те, як ви підтверджуєте особу, залежить від того, як розгорнуто екземпляр. Екземпляр у режимі local приймає електронну адресу і пароль. В екземплярі hosted або selfHosted вхід за паролем повністю відхиляється з 403 PASSWORD_AUTH_DISABLED, і єдиний спосіб увійти — це налаштований провайдер ідентичності, тож обліковий запис ніколи не отримає другого, слабшого шляху ідентифікації поруч із ним.

Вхід через провайдера ідентичності

Єдиний вхід (SSO) — це обмін кодом авторизації 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. З 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, який передає стрічку активності.

Браузери не можуть встановлювати заголовки під час handshake 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.

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 тепер містить поновлений токен поновлення.

Список каналів, які ви бачите

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.