API
모든 Nessie 클라이언트의 기반이 되는 HTTP API와 인증 방법을 설명합니다.
API란 무엇인가
Nessie에는 HTTP API가 하나뿐입니다. 관리자 웹 앱, macOS와 iOS 클라이언트, 원격 실행기 데몬은 모두 이 API의 일반 클라이언트이며, 이들이 할 수 있는 일은 다른 방식으로는 접근할 수 없습니다. Fastify 서버 위에서 동작하는 JSON API이며 GraphQL이 아닙니다. 별도의 “공개” 하위 집합도 없습니다. 여러분의 스크립트가 호출하는 표면이 곧 제품이 호출하는 표면입니다.
경로에는 버전 세그먼트가 없습니다. 모든 경로는 /api 아래에 있으며, 예외는 인바운드 MCP 엔드포인트 POST /mcp와 몇 가지 /.well-known/ 탐색 문서뿐입니다. 호환성은 필드를 추가하기만 하는 방식으로 관리합니다. 그래서 인식하지 못하는 내용을 무시하는 클라이언트는 계속 정상적으로 작동합니다.
기본 URL
Nessie는 직접 호스팅하는 제품이므로, 기본 URL은 배포 환경이 API를 제공하는 출처(origin)입니다. 운영자는 이 값을 NESSIE_API_PUBLIC_URL로 설정하며, API가 자체 절대 URL을 만들어야 할 때도 같은 값을 사용합니다. 저희가 운영하는 인스턴스에서는 https://api.nessie.works입니다. pnpm dev로 시작한 개발 인스턴스는 http://localhost:5454에서 응답합니다.
관리자 웹 앱은 API와 다른 출처이므로, 브라우저 클라이언트에는 CORS 허용 목록(NESSIE_CORS_ORIGINS)이 적용됩니다. 스크립트나 서버에서 보내는 요청에는 확인할 출처가 없으므로 영향을 받지 않습니다.
인증
의도적으로 공개된 소수의 경로를 제외한 모든 요청은 표준 헤더에 액세스 토큰을 담습니다:
Authorization: Bearer <token>토큰은 API가 발급하는 서명된 JWT입니다. 수명이 짧아 기본값은 30분(NESSIE_AUTH_TOKEN_TTL)이며, 일반 호출자가 제시하는 유일한 자격 증명입니다. Nessie에는 장기간 유효한 개인 API 키가 없습니다. 스크립트가 API를 호출하려면 사람의 브라우저와 같은 방식으로 로그인하고, 진행하면서 토큰을 갱신하면 됩니다.
토큰 받기
POST /api/auth/session은 세션을 발급합니다. 응답에는 액세스 토큰과 호출자의 신원이 담기고, 갱신 토큰은 별도의 HTTP 전용 쿠키로 설정됩니다.
본인임을 증명하는 방식은 인스턴스를 어떻게 배포했는지에 따라 달라집니다. local 모드로 실행되는 인스턴스는 이메일과 비밀번호를 받습니다. hosted 또는 selfHosted 인스턴스에서는 비밀번호 로그인이 403 PASSWORD_AUTH_DISABLED로 완전히 거부되며, 유일한 로그인 경로는 설정된 ID 제공자입니다. 그래서 계정에 그보다 약한 두 번째 신원 경로가 생길 수 없습니다.
ID 제공자를 통한 로그인
싱글 사인온은 PKCE를 사용하는 OAuth 인가 코드 교환이며, 세 단계로 이루어집니다. Nessie 자체의 ID 제공자는 UnlikeOtherAI이며, 인스턴스가 미러링하는 조직과 팀 구조도 이곳이 소유합니다. 아래의 교환 방식은 설정된 모든 제공자에 대해 같은 형태입니다.
GET /api/auth/providers는 이 인스턴스가 허용하는 제공자 목록을 반환합니다. 각 항목에는providerId, 표시 이름, 그리고 로그인 페이지가 해당 제공자로 자동 이동해야 하는지 여부가 담겨 있습니다.- 쿼리 매개변수로
codeChallenge,redirectUri,state를 담아GET /api/auth/providers/:providerId/authorize를 호출하면{ data: { authorizeUrl } }가 반환됩니다. 사용자를 그곳으로 보내세요. - 제공자는 코드와 함께 사용자를
redirectUri로 돌려보냅니다. 그 코드를{ providerId, code, codeVerifier, redirectUri }와 함께POST /api/auth/session에 다시 보내세요. 네 가지 값은 모두 함께 있어야 하며, 일부만 있으면400 EXTERNAL_AUTH_INCOMPLETE가 반환됩니다.
세션 갱신
갱신 토큰은 응답 본문에 절대 담기지 않습니다. 이름이 nessie_refresh인 HTTP 전용 쿠키로 설정되며, 범위는 /api/auth 경로로 한정되고, 기본 수명은 30일(NESSIE_AUTH_REFRESH_TOKEN_TTL)입니다. 이 쿠키를 담아 POST /api/auth/refresh를 호출하면 쿠키가 소비되고, 새 액세스 토큰이 발급되며, 새 쿠키가 그 자리를 대신해 설정됩니다. 토큰은 교체되므로, 도난당한 토큰은 정상 클라이언트가 다음에 갱신할 때까지만 유효합니다.
여러 계정에 로그인한 브라우저는 로그인마다 이런 쿠키를 하나씩 보관하며, 이름은 nessie_refresh_<slot>입니다. 요청이 어느 쿠키에 관한 것인지는 x-nessie-account-slot 헤더로 지정합니다. 그런 헤더를 보내지 않는 클라이언트의 요청은 nessie_refresh에 관한 것으로 간주됩니다. 명령줄 클라이언트에는 이것으로 충분합니다.
따라서 명령줄 클라이언트에는 쿠키 저장소가 필요합니다. 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로 응답합니다. 원래 오류 텍스트는 기록만 되고 응답으로 반환되지 않습니다. 밖으로 나가는 과정에서 요청 데이터가 섞여 들어갔을 수 있기 때문입니다.
먼저 마주치게 될 오류 코드
| 상태 | 코드 | 의미 |
|---|---|---|
| 400 | VALIDATION_ERROR | 본문이나 쿼리가 스키마와 일치하지 않습니다. |
| 401 | AUTH_REQUIRED | 베어러 토큰이 없거나, 헤더 형식이 잘못되었습니다. |
| 401 | TOKEN_EXPIRED | 액세스 토큰이 만료되었습니다. 갱신한 뒤 다시 시도하세요. |
| 401 | TOKEN_REVOKED | 세션이 로그아웃되었거나, 모든 세션이 강제로 종료되었습니다. |
| 403 | ACCOUNT_DEACTIVATED | 이 조직의 구성원 자격이 비활성화되었습니다. |
| 404 | NOT_FOUND | 해당 경로가 없거나, 이 호출자에게 보이는 레코드가 없습니다. |
| 429 | RATE_LIMITED | 요청 한도 버킷이 가득 찼습니다. Retry-After를 확인하세요. |
| 500 | INTERNAL_ERROR | 처리되지 않은 서버 오류이며, 의도적으로 내용을 드러내지 않습니다. |
페이지네이션
목록 엔드포인트는 오프셋이 아니라 불투명한 키셋 커서로 페이지를 나눕니다. 누군가 읽는 동안에도 행이 새로 추가되며, 오프셋을 쓰면 두 번째 페이지가 첫 번째 페이지에서 이미 보여 준 레코드를 조용히 반복하기 때문입니다.
쿼리 매개변수는 세 가지가 허용됩니다: 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당 10회, 이메일 주소당 5회이고, 메시지 게시는 분당 60회, /api/agents 아래 쓰기는 분당 60회입니다. 모든 임계값은 배포 환경마다 설정할 수 있으므로, 이 수치는 제공되는 기본값으로 보고 계약으로 여기지 마세요. 상태 확인과 실시간 스트림은 적용 대상에서 제외됩니다.
실시간
목록을 주기적으로 조회하는 방식은 대화를 따라가는 잘못된 방법입니다. 대신 세 가지 스트리밍 엔드포인트가 있습니다:
GET /api/events/stream— 로그인한 사람이 볼 수 있는 모든 항목에 대한 서버 전송 이벤트입니다.GET /api/threads/:threadId/stream— 한 스레드에 대한 서버 전송 이벤트입니다.Last-Event-ID를 지원하므로, 다시 연결하면 처음부터 시작하지 않고 이어서 받습니다.GET /api/activity— 활동 피드를 전달하는 웹소켓입니다.
브라우저는 웹소켓 핸드셰이크에서 헤더를 설정할 수 없으므로, 웹소켓 경로는 액세스 토큰을 ?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 | 파일을 먼저 업로드한 다음, 반환된 ID를 메시지에 연결합니다. |
| 검색 | /api/messages/search | 메시지 검색. 조직 전체 또는 한 채널로 범위를 좁힐 수 있습니다. |
| 시크릿과 설정 | /api/secrets, /api/settings/scoped | 명시적 권한 부여를 거치는 쓰기 전용 자격 증명 저장소와 계층형 설정 값. |
| 연결된 도구 | /api/mcp | 이 인스턴스가 호출하는 MCP 서버입니다. MCP와 연결된 도구를 참조하세요. |
| 상태 확인 | /api/health | 생존 상태(liveness)와 준비 상태(readiness), 둘 다 공개입니다. /api/health/ready는 데이터베이스도 확인합니다. |
권한은 그룹이 아니라 경로마다 적용됩니다. 대략 말하면, 감사 기록, 비용 원장, 정책 규칙, 추론 제어 플레인은 조직의 소유자와 관리자가 쓰는 영역이고, 대화, 프로젝트, 지식, 승인은 모든 구성원에게 열려 있으며 각 구성원이 실제로 볼 수 있는 범위로 좁혀집니다.
예시
로그인하고 갱신 쿠키 보관하기
local 모드 인스턴스에서 비밀번호로 로그인하는 경우입니다. 액세스 토큰은 응답 본문으로 돌아오고, 갱신 토큰은 쿠키 저장소에 저장됩니다.
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
}
}
}
}토큰이 만료되면 쿠키를 새 토큰과 교환하세요:
curl -sS -X POST https://api.example.com/api/auth/refresh \
--cookie nessie.cookies --cookie-jar nessie.cookies응답은 같은 { token, me } 형태이며, 쿠키 저장소에는 이제 교체된 갱신 토큰이 들어 있습니다.
볼 수 있는 채널 목록 조회하기
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에 보관해야 합니다.