nessie

API

API HTTP di balik setiap klien Nessie, dan cara melakukan autentikasi terhadapnya.

Apa itu API

Nessie memiliki satu API HTTP. Aplikasi web admin, klien macOS dan iOS, serta daemon eksekutor jarak jauh adalah klien biasa dari API ini, dan tidak ada yang bisa mereka lakukan yang dapat dijangkau dengan cara lain. API ini berupa JSON API di atas server Fastify, bukan GraphQL, dan tidak memiliki subset “publik” tersendiri: yang dipanggil skrip Anda sendiri adalah permukaan yang sama dengan yang dipanggil produk.

Tidak ada segmen versi dalam path. Setiap rute berada di bawah /api, kecuali endpoint MCP masuk di POST /mcp dan beberapa dokumen penemuan /.well-known/. Kompatibilitas dijaga dengan hanya pernah menambahkan field, sehingga klien yang mengabaikan hal yang tidak dikenalinya tetap bisa berfungsi.

URL dasar

Nessie dihosting sendiri, sehingga URL dasarnya adalah origin tempat deployment Anda melayani API. Operator menetapkannya sebagai NESSIE_API_PUBLIC_URL, dan nilai yang sama dipakai API ketika perlu membentuk URL absolut miliknya sendiri. Pada instans yang kami jalankan, alamatnya adalah https://api.nessie.works; instans pengembangan yang dijalankan dengan pnpm dev merespons di http://localhost:5454.

Aplikasi web admin berada di origin yang berbeda dari API, sehingga klien browser tunduk pada daftar izin CORS (NESSIE_CORS_ORIGINS). Permintaan dari skrip atau server tidak memiliki origin untuk diperiksa, dan tidak terpengaruh.

Autentikasi

Setiap permintaan di luar sejumlah kecil rute yang memang dibuat publik membawa access token dalam header standar:

Authorization: Bearer <token>

Token adalah JWT bertanda tangan yang diterbitkan oleh API. Masa berlakunya singkat, tiga puluh menit secara default (NESSIE_AUTH_TOKEN_TTL), dan ini satu-satunya kredensial yang pernah dikirim pemanggil biasa. Nessie tidak memiliki kunci API pribadi yang berlaku lama. Jika Anda ingin sebuah skrip memanggil API, skrip itu masuk dengan cara yang sama seperti browser seseorang, dan memperbarui tokennya selama berjalan.

Mendapatkan token

POST /api/auth/session membuat sesi. Responsnya berisi access token dan identitas pemanggil, serta menyetel token pembaruan sebagai cookie HTTP-only yang terpisah.

Cara Anda membuktikan identitas bergantung pada cara instans di-deploy. Instans yang berjalan dalam mode local menerima email dan kata sandi. Pada instans hosted atau selfHosted, masuk dengan kata sandi ditolak sepenuhnya dengan 403 PASSWORD_AUTH_DISABLED, dan satu-satunya jalan masuk adalah penyedia identitas yang dikonfigurasi, sehingga akun tidak pernah bisa memiliki jalur identitas kedua yang lebih lemah di sampingnya.

Masuk melalui penyedia identitas

Single sign-on adalah pertukaran kode otorisasi OAuth dengan PKCE, dalam tiga langkah. Penyedia identitas Nessie sendiri adalah UnlikeOtherAI, yang juga memiliki struktur organisasi dan tim yang dicerminkan sebuah instans; pertukaran di bawah ini berbentuk sama untuk penyedia mana pun yang dikonfigurasi.

  1. GET /api/auth/providers mencantumkan apa saja yang diterima instans ini, masing-masing dengan providerId, label, dan keterangan apakah halaman masuk harus langsung mengarahkan ke penyedia tersebut secara otomatis.
  2. GET /api/auth/providers/:providerId/authorize, dengan codeChallenge, redirectUri, dan state sebagai parameter query, merespons dengan { data: { authorizeUrl } }. Arahkan orang tersebut ke sana.
  3. Penyedia mengembalikan mereka ke redirectUri Anda dengan sebuah kode. Kirim kode itu kembali sebagai POST /api/auth/session dengan { providerId, code, codeVerifier, redirectUri }. Keempatnya wajib dikirim bersamaan, dan kumpulan yang tidak lengkap menghasilkan 400 EXTERNAL_AUTH_INCOMPLETE.

Memperbarui sesi

Token pembaruan tidak pernah berada di isi respons. Token ini disetel sebagai cookie HTTP-only bernama nessie_refresh, dengan cakupan path /api/auth, dan masa berlaku tiga puluh hari secara default (NESSIE_AUTH_REFRESH_TOKEN_TTL). Memanggil POST /api/auth/refresh dengan cookie tersebut akan menghabiskannya, menerbitkan access token baru, dan menyetel cookie baru menggantikannya. Token dirotasi, sehingga token yang dicuri hanya berlaku sampai klien yang asli melakukan pembaruan berikutnya.

Browser yang masuk ke beberapa akun menyimpan satu cookie seperti itu untuk setiap sesi masuk, bernama nessie_refresh_<slot>, dan menyebutkan cookie mana yang dimaksud sebuah permintaan melalui header x-nessie-account-slot. Klien yang tidak mengirim header tersebut berarti nessie_refresh, dan itulah yang dibutuhkan klien baris perintah.

Karena itu, klien baris perintah membutuhkan cookie jar. Dengan curl, itu berarti --cookie-jar pada panggilan masuk dan --cookie pada pembaruan, seperti dalam contoh kerja di bawah.

Yang diperiksa pada setiap permintaan

Autentikasi bukan hanya pemeriksaan tanda tangan. Sebelum handler berjalan, API memverifikasi tanda tangan dan masa berlaku token, memastikan sesi belum dicabut (baik melalui versi token tingkat pengguna maupun melalui id sesi individual), dan memuat keanggotaan pemanggil di organisasi yang disebut token. Keanggotaan yang dinonaktifkan menghasilkan 403. Ketika organisasi didukung oleh penyedia identitas eksternal, status pemanggil saat ini diperiksa ulang ke penyedia tersebut pada setiap permintaan, bukan dibaca dari token, sehingga akses yang dicabut di penyedia identitas langsung berhenti berlaku alih-alih menunggu masa berlaku berikutnya habis.

Kredensial yang bukan token sesi

Ada dua kredensial bearer lain, dan keduanya bukan kunci serbaguna. Kredensial perangkat suara hanya diterima pada rute panggilan suara, dan kredensial akses agen hanya diterima pada endpoint MCP. Menyajikan salah satunya di tempat lain akan ditolak dengan 403 yang menyebut nama kredensial tersebut, bukan dicoba ulang sebagai sesi yang lebih lemah. Lihat MCP dan alat yang terhubung untuk cara kredensial agen diterbitkan.

Format respons

Setiap respons yang berhasil berupa objek dengan kunci data. Tidak ada array mentah dan tidak ada payload di tingkat atas, sehingga klien tidak perlu membedakan bentuk respons:

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

Endpoint daftar menambahkan objek meta di sampingnya. Tidak ada hal lain yang pernah ditambahkan di tingkat atas.

Kegagalan juga menggunakan satu bentuk, apa pun penyebabnya:

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

code adalah bagian yang dapat dibaca mesin dan menjadi acuan percabangan Anda; message ditujukan untuk manusia. field dan details hadir ketika kegagalan cukup spesifik untuk ditunjuk. Galat server yang tidak tertangani selalu berupa 500 INTERNAL_ERROR dengan pesan tetap. Teks galat yang mendasarinya dicatat, tidak pernah dikembalikan, karena teks itu bisa saja menyerap data permintaan dalam perjalanannya keluar.

Kode galat yang akan paling sering Anda temui

StatusKodeArti
400VALIDATION_ERRORIsi atau query tidak sesuai dengan skema.
401AUTH_REQUIREDTidak ada bearer token, atau header-nya tidak valid.
401TOKEN_EXPIREDAccess token sudah melewati masa berlakunya. Perbarui lalu coba lagi.
401TOKEN_REVOKEDSesi telah keluar, atau semua sesi dipaksa keluar.
403ACCOUNT_DEACTIVATEDKeanggotaan di organisasi ini telah dinonaktifkan.
404NOT_FOUNDRute tidak ada, atau tidak ada data yang terlihat oleh pemanggil ini.
429RATE_LIMITEDSebuah bucket sudah penuh. Baca Retry-After.
500INTERNAL_ERRORGalat server yang tidak tertangani, sengaja dibuat tidak terbuka.

Paginasi

Endpoint daftar memberi halaman dengan kursor keyset yang buram, bukan offset. Baris disisipkan saat seseorang sedang membaca, dan dengan offset, halaman kedua diam-diam mengulang data yang sudah ditampilkan di halaman pertama.

Ada tiga parameter query yang diterima: cursor, limit, dan direction (forward atau backward). Halaman default berisi 25 data dan batas atasnya 100; nilai yang lebih besar dipotong, bukan ditolak. Endpoint yang juga mengurutkan atau menyaring menambahkan sort, order, dan q berupa teks bebas.

meta halaman memberi tahu Anda posisi Anda:

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

nextCursor dan prevCursor sama-sama tersedia: Sebelumnya dan Berikutnya adalah dua kontrol yang diharapkan orang. Keduanya buram. Server menyematkan kunci urutannya sendiri di dalamnya, dan klien yang mengurai atau membentuk salah satunya akan rusak begitu daftar mengubah cara pengurutannya. total hadir di sebagian besar daftar dan dihilangkan jika penghitungan tidak bermakna, misalnya pada hasil pencarian berperingkat.

Batas laju

Batas laju berlaku per jendela tetap dan diterapkan sebelum isi permintaan bahkan dibaca. Permintaan yang ditolak berupa 429 dengan kode RATE_LIMITED dan header Retry-After dalam hitungan detik.

Sebagian besar bucket menggunakan IP klien sebagai kunci. Bucket yang sensitif terhadap kredensial (masuk, pembaruan, verifikasi ulang kata sandi, dan penulisan kredensial MCP) juga menyimpan penghitung per akun dan menolak jika salah satu terpicu, sehingga satu alamat yang menyemprot banyak akun maupun satu akun yang diserang dari banyak alamat sama-sama menabrak tembok. Masuk secara default 10 percobaan per IP dan 5 per alamat email dalam sepuluh menit; mengirim pesan secara default 60 per menit; penulisan di bawah /api/agents 60 per menit. Setiap ambang dapat dikonfigurasi per deployment, jadi anggap angka-angka ini sebagai default yang dikirim, bukan kontrak. Pemeriksaan kesehatan dan aliran real-time tidak termasuk.

Real-time

Melakukan polling daftar adalah cara yang salah untuk mengikuti sebuah percakapan. Sebagai gantinya, ada tiga endpoint streaming:

  • GET /api/events/stream — server-sent events untuk semua yang dapat dilihat orang yang masuk.
  • GET /api/threads/:threadId/stream — server-sent events untuk satu utas. Endpoint ini menghormati Last-Event-ID, sehingga koneksi ulang melanjutkan, tidak memulai ulang.
  • GET /api/activity — WebSocket yang membawa feed aktivitas.

Browser tidak dapat mengatur header pada handshake WebSocket, sehingga rute WebSocket juga menerima access token sebagai parameter query ?token=. Itu satu-satunya tempat token boleh berada di URL; setiap rute lain menerima header.

Kelompok sumber daya

Ada lebih dari seribu endpoint, terlalu banyak untuk didaftar satu per satu. Endpoint ini dikelompokkan ke dalam keluarga, dan begitu Anda mengetahui awalan sebuah keluarga, rute-rute individualnya dapat diprediksi: sebuah koleksi di awalan itu, sebuah record di bawahnya, dan tindakan bernama sebagai sub-path POST.

KeluargaAwalanMencakup
Autentikasi/api/authMasuk, pembaruan, keluar, identitas saat ini, dan sesi milik seseorang sendiri.
Kanal dan utas/api/channels, /api/threadsModel percakapan: kanal menampung utas, utas menampung pesan, dan balasan bersarang di bawah root.
Agen/api/agentsDefinisi agen, binding kanal mereka, percakapan, daftar tugas, dan statusnya.
Proyek dan papan/api/projects, /api/tasks, /api/iterationsPelacakan pekerjaan: papan dan kolom di bawah proyek, tiket di atasnya, dan iterasi di atas itu.
Pengetahuan/api/knowledge-baseRuang berisi halaman dan berkas yang diberi versi, dengan komentar, tautan balik, dan pencarian.
Surat/api/mail, /api/mailbox-connectionsKotak surat tersambung, kotak surat agen, dan draf yang ditulis agen untuk disetujui seseorang.
Persetujuan/api/approvalsAntrean tindakan yang tidak boleh diambil agen secara sepihak, dan satu panggilan untuk menyelesaikannya.
Audit/api/audit-logJejak audit organisasi berbentuk rantai hash, dengan endpoint verifikasi. Pemilik dan admin.
Biaya dan penggunaan/api/ledger, /api/billingAkuntansi token dan biaya, anggaran, serta waktu eksekusi; terpisah, kredit dan isi ulang saldo. Pemilik dan admin.
Organisasi dan tim/api/organizations, /api/teams, /api/usersAdministrasi organisasi, tim, dan keanggotaan, termasuk undangan.
Pemicu/api/triggersJadwal dan webhook yang memulai pekerjaan agen, beserta riwayat pemicunya.
Panggilan dan suara/api/calls, /api/voicePanggilan dalam kanal, serta sesi asisten suara, transkripnya, dan token perangkatnya.
Eksekutor/api/executors, /api/execution-environment-templatesMesin tempat agen bekerja, lingkungannya, dan saluran pemasangan daemon.
Eksekusi/api/runsEksekusi agen yang sedang berjalan: batalkan, lanjutkan, mulai ulang, atau ikat ke eksekutor.
Unggahan/api/uploads, /api/attachmentsUnggah berkas terlebih dahulu, lalu tautkan id yang dikembalikan ke sebuah pesan.
Pencarian/api/messages/searchPencarian pesan, untuk seluruh organisasi atau dibatasi pada satu kanal.
Rahasia dan pengaturan/api/secrets, /api/settings/scopedPenyimpanan kredensial hanya-tulis dengan hibah eksplisit, dan nilai konfigurasi berlapis.
Alat terhubung/api/mcpServer MCP yang dipanggil instans ini. Lihat MCP dan alat yang terhubung.
Kesehatan/api/healthPemeriksaan kesiapan dan kelangsungan hidup, keduanya publik. /api/health/ready juga memeriksa basis data.

Hak akses ditegakkan per rute, bukan per keluarga. Secara garis besar: log audit, buku besar biaya, aturan kebijakan, dan bidang kendali inferensi diperuntukkan bagi pemilik dan admin organisasi, sedangkan percakapan, proyek, pengetahuan, dan persetujuan terbuka bagi anggota mana pun dan dipersempit sesuai apa yang sebenarnya dapat dilihat anggota tersebut.

Contoh kerja

Masuk dan simpan cookie pembaruan

Pada instans mode local, dengan kata sandi. Access token dikembalikan di isi respons; token pembaruan masuk ke 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
      }
    }
  }
}

Ketika token berakhir masa berlakunya, tukarkan cookie dengan yang baru:

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

Responsnya berbentuk { token, me } yang sama, dan cookie jar sekarang menyimpan token pembaruan yang sudah dirotasi.

Daftar kanal yang dapat Anda lihat

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

Tambahkan ?teamId=… untuk mempersempit ke satu tim, atau ?includeArchived=true untuk menyertakan kanal yang diarsipkan. defaultThreadId adalah tempat Anda mengirim pesan berikutnya.

Kirim pesan

Pesan harus berisi content atau setidaknya satu lampiran yang diunggah. Kirim Idempotency-Key, dan permintaan yang dicoba ulang akan menghasilkan pesan yang dibuat oleh percobaan pertama, bukan menayangkan salinan kedua.

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

Pesan baru berupa 201:

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

Pengulangan dengan kunci idempotensi yang sama dijawab 200 dengan pesan asli dan pendingAgentInvites yang kosong, karena percobaan pertama sudah melakukan pemberitahuan dan pengiriman yang dijelaskan oleh array tersebut.

Dua penolakan pada rute ini layak ditangani secara eksplisit. Konten yang melewati batas panjang obrolan berupa 413 MESSAGE_TOO_LARGE, dan respons yang tepat adalah mengunggah teks sebagai berkas. Konten yang secara struktur menyerupai kredensial berupa 422 SECRET_INTERCEPTED. Server menolaknya sebelum pesan disimpan, dikirim, atau dicatat, dan kredensial seharusnya ditempatkan di /api/secrets.