nessie

API

すべてのNessieクライアントを支えるHTTP APIと、その認証方法です。

APIとは

Nessieには、HTTP APIが一つだけあります。管理者用Webアプリ、macOSとiOSのクライアント、リモートのエグゼキューター・デーモンは、いずれもこのAPIの通常のクライアントです。それらができることは、ほかの経路からは一切到達できません。Fastifyサーバー上のJSON APIであり、GraphQLではありません。「公開」向けに分けられた別のサブセットもありません。お客様のスクリプトが呼び出すAPIは、製品が呼び出すAPIと同じです。

パスにバージョンの区分はありません。すべてのルートは/apiの下にあります。ただし、POST /mcpの受信用MCPエンドポイントと、いくつかの/.well-known/のディスカバリー文書は例外です。互換性は、フィールドを追加するだけにとどめることで保たれています。そのため、認識できない項目を無視するクライアントは、引き続き動作します。

ベースURL

Nessieはセルフホスト型のため、ベースURLは、お使いの環境でAPIを提供しているオリジンです。運用者がNESSIE_API_PUBLIC_URLとして設定します。APIが独自の絶対URLを生成する必要がある場合も、同じ値を使います。当社が運用するインスタンスではhttps://api.nessie.worksです。pnpm devで起動した開発用インスタンスは、http://localhost:5454で応答します。

管理者用Webアプリは、APIとは別のオリジンにあります。そのため、ブラウザのクライアントはCORSの許可リスト(NESSIE_CORS_ORIGINS)の対象になります。スクリプトやサーバーからのリクエストには確認すべきオリジンがないため、影響を受けません。

認証

公開用に意図的に設けた少数のルートを除き、すべてのリクエストは、標準のヘッダーにアクセストークンを付けて送ります:

Authorization: Bearer <token>

トークンは、APIが発行する署名付きのJWTです。有効期間は短く、初期設定では30分です(NESSIE_AUTH_TOKEN_TTL)。通常の呼び出し元が提示する認証情報は、これだけです。Nessieには、長期間有効な個人用APIキーはありません。スクリプトからAPIを呼び出す場合は、人のブラウザと同じ方法でログインし、その後は必要に応じてトークンを更新します。

トークンの取得

POST /api/auth/sessionはセッションを発行します。アクセストークンと呼び出し元のIDを応答し、更新用トークンは別のHTTP専用Cookieとして設定します。

本人確認の方法は、インスタンスのデプロイ方法によって異なります。localモードで動作するインスタンスは、メールアドレスとパスワードを受け付けます。hostedまたはselfHostedのインスタンスでは、パスワードによるログインは403 PASSWORD_AUTH_DISABLEDで完全に拒否されます。入る方法は、設定済みのIDプロバイダーだけです。そのため、アカウントにそれより弱いIDの経路が並んで増えることはありません。

IDプロバイダー経由のログイン

シングルサインオンは、PKCEを用いたOAuthの認可コード交換で、三つの手順で行われます。Nessie独自のIDプロバイダーはUnlikeOtherAIです。UnlikeOtherAIは、インスタンスが反映する組織とチームの構造も管理しています。以下の交換は、設定されたどのプロバイダーでも同じ形です。

  1. GET /api/auth/providersは、このインスタンスが受け付けるプロバイダーを一覧で返します。各プロバイダーには、providerId、表示名、ログインページから自動的にリダイレクトするかどうかが含まれます。
  2. GET /api/auth/providers/:providerId/authorizeは、クエリパラメーターとしてcodeChallenge、redirectUri、stateを受け取り、{ data: { authorizeUrl } }で応答します。その応答先へ本人を誘導してください。
  3. プロバイダーは、コードを付けて本人をredirectUriへ戻します。そのコードを、{ providerId, code, codeVerifier, redirectUri }とともにPOST /api/auth/sessionへ送り返してください。四つの値はすべて一緒に必要です。一部でも欠けている場合は、400 EXTERNAL_AUTH_INCOMPLETEになります。

セッションの更新

更新用トークンがレスポンスの本文に含まれることはありません。nessie_refreshという名前のHTTP専用Cookieとして設定され、スコープは/api/authパスです。初期設定の有効期間は30日です(NESSIE_AUTH_REFRESH_TOKEN_TTL)。そのCookieを付けてPOST /api/auth/refreshを呼び出すと、Cookieは使い切られ、新しいアクセストークンが発行され、代わりに新しいCookieが設定されます。トークンは毎回入れ替わるため、盗まれたトークンが有効なのは、本物のクライアントが次に更新するまでの間だけです。

複数のアカウントにログインしたブラウザは、ログインごとにこうしたCookieを一つずつ保持します。Cookieの名前はnessie_refresh_<slot>です。リクエストが対象とするCookieは、x-nessie-account-slotヘッダーで指定します。そのヘッダーを送らないクライアントは、nessie_refreshを対象とします。コマンドラインのクライアントには、これで十分です。

そのため、コマンドラインのクライアントにはCookieジャーが必要です。curlを使う場合は、ログイン時に--cookie-jarを、更新時に--cookieを指定します。以下の実例のとおりです。

すべてのリクエストで確認される内容

認証は、署名の確認だけではありません。ハンドラーが実行される前に、APIはトークンの署名と有効期限を検証し、セッションが取り消されていないことを確認します(ユーザー全体のトークンバージョンと、個々のセッションIDの両方で確認します)。そのうえで、トークンに記載された組織における呼び出し元のメンバーシップを読み込みます。無効化されたメンバーシップは403になります。組織が外部のIDプロバイダーに基づく場合は、トークンから読み取るのではなく、リクエストのたびにそのプロバイダーに対して呼び出し元の現在の資格を再確認します。そのため、上流で取り消されたアクセス権は、次の有効期限を待たずに直ちに無効になります。

セッショントークン以外の認証情報

ほかに二種類のベアラー認証情報があり、いずれも汎用のキーではありません。音声デバイスの認証情報は、音声通話のルートでのみ受け付けられます。エージェントのアクセス用認証情報は、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ベアラートークンがない、またはヘッダーの形式が正しくありません。
401TOKEN_EXPIREDアクセストークンの有効期限が切れています。更新してから再試行してください。
401TOKEN_REVOKEDセッションからログアウトされたか、すべてのセッションが強制的に終了されています。
403ACCOUNT_DEACTIVATEDこの組織のメンバーシップは無効化されています。
404NOT_FOUND該当するルートがない、または呼び出し元から閲覧できるレコードがありません。
429RATE_LIMITEDバケットが上限に達しています。Retry-Afterを確認してください。
500INTERNAL_ERROR処理されなかったサーバーエラーです。意図的に詳細を伏せています。

ページング

一覧のエンドポイントは、オフセットではなく、不透明なキーセットカーソルでページングします。誰かが読み取っている間にも行が追加されるためです。オフセットを使うと、二ページ目が一ページ目で表示済みのレコードを、気づかれないまま繰り返してしまいます。

受け付けるクエリパラメーターは三つです。cursor、limit、direction(forwardまたはbackward)。初期設定のページサイズは25件で、上限は100件です。上限を超える値は拒否されず、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アドレスあたり10回、メールアドレスあたり5回です。メッセージの投稿は毎分60回が初期設定です。/api/agents配下への書き込みは、毎分60回です。すべての上限はデプロイごとに設定できます。ここに示すのは出荷時の初期値であり、契約上の保証ではありません。ヘルスチェックとリアルタイムのストリームは対象外です。

リアルタイム

会話を追いかけるのに、一覧を定期的に問い合わせるのは適切ではありません。代わりに、三つのストリーミング用エンドポイントがあります:

  • GET /api/events/stream:ログインしている人が閲覧できるすべてのイベントを、サーバー送信イベントで配信します。
  • GET /api/threads/:threadId/stream:一つのスレッドのサーバー送信イベントです。Last-Event-IDに対応しているため、再接続時は最初からではなく、途中から再開します。
  • GET /api/activity:アクティビティフィードを配信するWebSocketです。

ブラウザは、WebSocketのハンドシェイクでヘッダーを設定できません。そのため、WebSocketのルートでは、アクセストークンを?token=というクエリパラメーターとしても受け付けます。URLにトークンを含めてよいのは、この場所だけです。ほかのすべてのルートは、ヘッダーでトークンを受け取ります。

リソースのファミリー

エンドポイントは1,000をはるかに超えており、すべてを挙げるには多すぎます。エンドポイントはファミリーにまとめられており、ファミリーのプレフィックスがわかれば、個々のルートは予測できます。プレフィックスにコレクションがあり、その下にレコードがあります。名前付きのアクションは、POSTのサブパスとして表されます。

ファミリープレフィックス対象範囲
認証/api/authログイン、更新、ログアウト、現在のID、本人のセッション。
チャンネルとスレッド/api/channels, /api/threads会話のモデル:チャンネルはスレッドを持ち、スレッドはメッセージを持ち、返信はルートの下にネストされます。
エージェント/api/agentsエージェントの定義、チャンネルとの紐付け、会話、ToDo、ステータス。
プロジェクトとボード/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エージェントの作業を開始するスケジュールとWebhook。実行履歴を含みます。
通話と音声/api/calls, /api/voiceチャンネル内の通話と、音声アシスタントのセッション、文字起こし、デバイストークン。
エグゼキューター/api/executors, /api/execution-environment-templatesエージェントが作業するマシン、その環境、デーモンのペアリング用チャンネル。
実行/api/runs実行中のエージェントの実行:キャンセル、継続、再開、エグゼキューターへの紐付け。
アップロード/api/uploads, /api/attachmentsまずファイルをアップロードし、返されたIDをメッセージに紐付けます。
検索/api/messages/searchメッセージ検索。組織全体、または一つのチャンネルに限定できます。
シークレットと設定/api/secrets, /api/settings/scoped明示的な権限付与を伴う書き込み専用の認証情報ストレージと、階層化された設定値。
接続済みツール/api/mcpこのインスタンスが呼び出す先のMCPサーバー。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": []
  }
}

同じIdempotency-Keyの再送には、200で元のメッセージが返され、pendingAgentInvitesは空になります。配列が示す通知と配信は、最初の試行ですでに行われているためです。

このルートでの拒否のうち、二つは個別に処理する価値があります。チャットの文字数上限を超える内容は413 MESSAGE_TOO_LARGEになります。その場合は、テキストをファイルとしてアップロードするのが正しい対応です。認証情報のように見える内容は422 SECRET_INTERCEPTEDになります。サーバーは、メッセージを保存、配信、記録する前に拒否します。認証情報は/api/secretsに保存してください。