# API v1

REST dan JSON. Aplikasi Onix sendiri memakai API yang sama, jadi data yang kamu lihat di aplikasi datang lewat endpoint ini.

Base URL: `https://onix.sassly.ai/api/v1`. Semua path di halaman ini relatif terhadap base URL. Body request berformat JSON (`Content-Type: application/json`). Spesifikasi lengkap OpenAPI 3.1: [`/docs/openapi.yaml`](https://onix.sassly.ai/docs/openapi.yaml) — bisa diimpor ke Postman, Insomnia, atau generator SDK.

## Autentikasi

|  | Aplikasi Onix (browser) | Integrasi (server-mu) |
| --- | --- | --- |
| Identitas | Cookie sesi setelah masuk dengan Google | Header `Authorization: Bearer onx_live_…` |
| Request yang mengubah data | Wajib header `X-CSRF-Token` milik sesi; aplikasi mengirimnya otomatis | Tanpa CSRF; key ber-scope `write` |
| Hak akses | Sesuai role dan akses channel pengguna | Seluruh workspace, dibatasi scope key |
| Paket | Semua paket | Pro dan Custom |

### Membuat API key

1. Buka **Pengaturan → API** (butuh akses *Kelola API key*; khusus Pro).
2. Klik **Buat key**, beri nama (mis. "Integrasi CRM"), lalu pilih scope: `read` (hanya membaca) atau `write` (membaca dan mengubah data).
3. Key berawalan `onx_live_` dan **hanya ditampilkan sekali**. Simpan di secret manager; jangan taruh di kode frontend atau repositori.
4. Maksimal 10 key aktif per workspace. Request dengan key yang sudah dicabut dibalas `401`.

```
export ONIX_API_KEY="onx_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

> API key bertindak atas nama workspace dan tidak dibatasi role, jadi perlakukan seperti password. Endpoint pengelolaan (anggota, role, pengaturan, API key, tagihan) hanya bisa dari aplikasi. API tidak mendukung CORS: panggil dari server, bukan dari JavaScript di browser.

## Format respons

Respons sukses selalu berbentuk `{"data": …, "meta": {…}}`; `meta` hanya ada bila relevan. Respons error selalu berbentuk:

```
{
  "error": {
    "code": "validation_error",
    "message": "Nama workspace 2–120 karakter.",
    "fields": {"name": "Nama workspace 2–120 karakter."}
  }
}
```

- `code` tetap dan aman dipakai di logika program; `message` adalah teks untuk manusia yang mengikuti bahasa (header `X-Locale: en|id`).
- `fields` hanya ada pada error input, berisi pesan per kolom.
- **Waktu selalu UTC** dalam ISO 8601, mis. `2026-10-08T02:30:00Z`. Ubah ke zona waktumu sendiri saat menampilkan; aplikasi Onix memakai zona pribadi pengguna atau zona workspace.
- Daftar berhalaman menerima `page` (mulai 1) dan `per_page` (maks. 100, bawaan 25 kecuali disebut lain), lalu mengembalikan `meta.page`, `meta.per_page`, `meta.total`, dan `meta.pages`.

## Batas request

- 120 request per menit per API key.
- Respons memuat header `X-RateLimit-Limit` dan `X-RateLimit-Remaining`.
- Lewat batas dibalas `429 rate_limited` dengan header `Retry-After` (detik sampai menit berikutnya).

## Kode error

| HTTP | code | Arti |
| --- | --- | --- |
| 401 | `unauthorized` | API key tidak ada, formatnya salah, atau sudah dicabut; atau sesi aplikasi sudah berakhir. |
| 402 | `plan_required` | Workspace bukan Pro atau Custom yang aktif (API key dan fitur khusus Pro). |
| 403 | `forbidden` | Tidak punya permission, endpoint khusus aplikasi dipanggil dengan API key, atau key `read` mencoba mengubah data. |
| 404 | `not_found` | Endpoint atau data tidak ada, termasuk data milik workspace lain. |
| 405 | `method_not_allowed` | Method tidak didukung endpoint ini; lihat header `Allow`. |
| 409 | `conflict` / `workspace_required` | Data bentrok dengan keadaan sekarang (mis. role masih dipakai), atau pengguna belum punya workspace. |
| 419 | `csrf_mismatch` | Request aplikasi selain GET tanpa `X-CSRF-Token` yang valid. Muat ulang halaman lalu coba lagi. |
| 422 | `validation_error` | Input tidak valid; rincian per kolom ada di `error.fields`. |
| 423 | `workspace_frozen` | Workspace dibekukan karena kontrak Custom berhenti. Hanya Paket & Tagihan, pindah kepemilikan, notifikasi, dan data baca dasar yang tetap jalan. |
| 429 | `rate_limited` | Terlalu banyak request; tunggu sesuai `Retry-After`. |
| 502 | `ai_error` | Penyedia AI sedang gangguan. Kuota AI tidak berkurang; coba lagi sebentar. |
| 503 | `ai_unavailable` | Fitur AI belum diatur di server Onix. Hubungi admin Onix. |
| 500 | `server_error` | Gangguan di server. Aman dicoba lagi beberapa saat kemudian. |

## Endpoint

Kolom Permission menunjukkan hak akses yang dibutuhkan pengguna aplikasi (diatur lewat role); "—" berarti semua anggota workspace. API key dianggap punya semua permission workspace dan melihat semua data.

**Batas data (tim)**: permission mengatur menu & aksi, sedangkan data percakapan & tiket yang terlihat mengikuti tim. Percakapan yang belum di-assign terlihat semua yang punya `interactions.view`; yang di-assign ke tim Frontline hanya terlihat anggota tim itu; yang di-assign ke orang hanya terlihat orang itu. Tiket tanpa penanggung jawab terlihat anggota tim Back Office-nya; tiket dengan penanggung jawab hanya terlihat peserta (penanggung jawab, pembuat, yang di-@mention). Artikel knowledge base khusus tim hanya terlihat anggota tim yang dipilih (tanpa tim = semua tim). Lead pipeline tanpa PJ terlihat anggota tim lead-nya; lead ber-PJ hanya terlihat PJ-nya. Owner, role admin, dan anggota "Bisa lihat semua data" melihat semuanya (akses channel tetap berlaku). Data yang tidak boleh dilihat dibalas 404.

### Bisa dipanggil dengan API key

| Endpoint | Permission | Keterangan |
| --- | --- | --- |
| GET `/me` | — | Pengguna, workspace aktif, role, permission, dan zona waktu efektif. Lewat API key: pengguna = pembuat key, `via` = `"key"`. |
| GET `/workspace` | — | Workspace aktif: nama, paket, masa aktif, zona waktu, dan batas paket. |
| GET `/dashboard` | `dashboard.view` | Langkah awal, pemakaian kuota, aktivitas 14 hari, aktivitas terbaru, dan ringkasan tim (bagian tertentu butuh permission tambahan). |
| GET `/channels` | — | Channel yang boleh diakses (`type` `whatsapp`/`email`/`livechat`/`api`, status, pengaturan, `icon_url`, `hours` = jam kerja sendiri atau `null`, `auto_reply`; livechat juga kunci publik & kode sematan, API juga URL webhook — tanpa kunci & secret), beserta kuota channel (gabungan semua jenis). |
| GET `/channels/{id}/icon` | — | Ikon/logo unggahan channel (pengelola channel atau anggota dengan akses channel itu); 404 bila memakai ikon bawaan. |
| GET `/interactions` | `interactions.view` | Inbox: percakapan per `kind` (`chat`/`group`) dan `tab` (`new` = belum dibalas sama sekali, `waiting` = menunggu balasan, `unreplied` = keduanya, `replied`, `solved`, `closed`). Setiap item punya `reply_state`. Filter `channel_ids` (satu atau beberapa id channel dipisah koma, mis. `1,3`; `channel_id` lama tetap diterima), `assignee` (`me`/`none`/id), `team` (`mine`/`none`/id), `label_id`, `contact_id`, `q`, `created_from`/`created_to` (tanggal masuk, YYYY-MM-DD zona pembaca), `category` (id = kategori itu beserta semua subkategorinya, atau `none`). `meta.counts` = jumlah per tab; `meta.teams` = tim Frontline untuk filter. Di Pro/Custom setiap item punya `sla` (target balas dalam jam kerja: `level` `warn`/`breach`, menit, tenggat). Hanya percakapan yang boleh dilihat (batas data tim). |
| GET `/interactions/{id}` | `interactions.view` | Satu percakapan: kontak/grup, channel, status, tim, assignee, label WhatsApp, waktu respons, `custom_fields` (field kustom interaction + nilainya), `notes_count`, `tickets` (SEMUA tiket dari percakapan ini — juga tanpa akses tiket — masing-masing dengan `can_open`, `update_request`, `sla`, dan `thread` dengan tim tiket), dan untuk pemegang `contacts.view` data & field kustom kontak (`contact.details`, `contact.custom_fields`). |
| POST `/interactions/{id}/tickets/{ticket}/messages` | `interactions.reply` | Dari percakapan ke salah satu tiketnya (tanpa perlu akses tiket): `{"text": "…"}` mengirim pesan ke diskusi tiket; `{"request_update": true}` (teks opsional) meminta update — tiket menampilkan `update_request` sampai tim mengirim feedback/kesimpulan atau menyelesaikannya. Hanya tiket aktif (422), paling sering sekali per 15 menit per tiket (429). Hasil `{"message", "tickets"}`. |
| GET `/interactions/{id}/notes` | `interactions.view` | Catatan internal percakapan (terbaru dulu, maks. 200), termasuk feedback & kesimpulan tiket (`meta.source`). Tulis catatan lewat `POST /interactions/{id}/messages` dengan `type` = `note`. |
| GET `/interactions/{id}/messages` | `interactions.view` | Pesan urut lama → baru. Kursor `before`/`after` (id pesan), `limit` maks. 100. |
| POST `/interactions/{id}/messages` | `interactions.reply` | Kirim pesan (scope `write`): `{"text": "…"}`, atau `multipart/form-data` dengan `file`. Jenis lain: `note`, `sticker`, `location`, `contact`; kutip dengan `reply_to_id`; template dengan `template_id`. Percakapan email: `html` (format minimal: tebal, miring, garis bawah, daftar, tautan; `text` dibuat otomatis), `cc` (dipisah koma, maks 10), `reply_all`, dan beberapa lampiran `files[]` (multipart, maks 10 file, total 20 MB); penerima, subjek "Re: …", kutipan email sebelumnya, dan tanda tangan diisi otomatis. Di nomor WhatsApp dengan inisial agen aktif, inisial pengirim ditambahkan di baris terakhir (tidak lewat API key). Percakapan livechat & channel API: teks, satu lampiran, atau catatan (tanpa kutip, stiker, lokasi, kontak); membalas livechat yang belum diambil sekaligus mengambilnya (tidak lewat API key). |
| PATCH `/interactions/{id}` | `interactions.reply` | Ubah `status` (`open`/`solved`/`closed`), `team_id` (tim Frontline), `assignee_id`, `category_id` (kategori percakapan, `null` = tanpa kategori), dan/atau `custom` (field kustom interaction `{kunci: nilai}`; nilai kosong menghapus). Memindah tim atau menugaskan orang lain butuh `interactions.assign`; orang harus anggota tim percakapan (admin boleh tanpa tim). Hasil `{"id", "visible": false}` bila pemanggil tidak lagi boleh melihatnya. |
| PATCH `/interactions/{id}/messages/{message}` | `interactions.reply` | Edit pesan teks sendiri (maks. 15 menit setelah dikirim). |
| DELETE `/interactions/{id}/messages/{message}` | `interactions.reply` | Tarik pesan (hapus untuk semua), maks. 2 hari. |
| POST `/interactions/{id}/messages/{message}/retry` | `interactions.reply` | Kirim ulang pesan yang gagal. |
| POST `/interactions/{id}/messages/{message}/reaction` | `interactions.reply` | Reaksi emoji: `{"emoji": "👍"}`; kosong = hapus. |
| GET `/interactions/{id}/assignees` | `interactions.reply` | Anggota yang bisa ditugaskan ke percakapan ini (dengan `team_ids`) dan tim Frontline beserta anggotanya di `meta.teams`. |
| GET `/messages/{id}/media` | `interactions.view` | File media pesan (privat). Mendukung `Range`; `?download=1` untuk mengunduh. |
| GET `/messages/{id}/email` | `interactions.view` | Email asli (HTML yang sudah dibersihkan, tanpa skrip) untuk ditampilkan di bingkai aman. Gambar dari internet diblokir; `?images=1` memuatnya. |
| GET `/messages/{id}/attachments/{index}` | `interactions.view` | Lampiran email ke-`index` (mulai 0, urutan `email.attachments` pesan). `?download=1` untuk mengunduh. |
| GET `/contacts/{id}/avatar` | `interactions.view` | Foto profil WhatsApp kontak. |
| GET `/groups/{id}/avatar` | `interactions.view` | Foto grup WhatsApp. |
| POST `/interactions` | `interactions.reply` | Mulai chat dengan kontak (scope `write`): `{"contact_id", "channel_id", "text"}`. Percakapan terbuka dengan kontak itu di nomor yang sama dipakai lagi. Channel email: wajib `subject`, isi `html` atau `text`, opsional `cc`; selalu membuat percakapan baru ke email kontak. |
| POST `/interactions/{id}/feedback-seen` | `interactions.view` | Tandai feedback/kesimpulan tiket di percakapan sudah dibaca. |
| GET `/groups/{id}` | `interactions.view` | Halaman grup: info, anggota (dicocokkan dengan kontak), dan percakapan grup. |
| GET `/tickets` | `tickets.view` | Daftar tiket yang boleh dilihat (batas data tim). Filter `status` (`active`, `open`, …), `priority`, `assignee` (`me`/`none`/id), `team` (`mine`/id), `q`, `contact_id`, `interaction_id`, `overdue`. `meta.counts` = jumlah per status, `meta.teams` = tim Back Office untuk filter. |
| POST `/tickets` | `tickets.create` | Buat tiket (scope `write`): `subject`, `team_id` (tim Back Office, wajib), `assignee_ids` (opsional, anggota tim itu), `description`, `priority`, `due_date`, `interaction_id` atau `contact_id`, dan `custom` (field kustom tiket `{kunci: nilai}`). Kuota tiket bulanan berlaku (402). |
| GET `/tickets/candidates` | `tickets.create` | Anggota yang bisa ditugaskan atau disebut di tiket, dengan `team_ids` Back Office & `is_admin`. |
| GET `/tickets/{id}` | `tickets.view` | Detail tiket, tim, peserta, izin pemanggil, `custom_fields` (field kustom tiket + nilainya), dan cuplikan percakapan asal. |
| PATCH `/tickets/{id}` | `tickets.manage` | Ubah `status`, `priority`, `due_date`, `subject`, `description`, `team_id` (pindah tim melepas penanggung jawab yang bukan anggota tim baru), atau `custom` (field kustom tiket; field yang berubah dicatat di log). |
| POST `/tickets/{id}/assignees` | `tickets.manage` | Tambah penanggung jawab (anggota tim tiket atau admin): `{"user_id": 5}`. |
| DELETE `/tickets/{id}/participants/{user}` | `tickets.manage` | Keluarkan peserta (pengikut boleh berhenti mengikuti sendiri). Tanpa penanggung jawab, tiket kembali terlihat seluruh tim. |
| GET `/tickets/{id}/messages` | `tickets.view` | Diskusi tiket + log di `meta.events`; `after_message`/`after_event` untuk yang baru saja. |
| POST `/tickets/{id}/messages` | `tickets.view` | Kirim pesan diskusi: `{"body": "…", "mention_ids": [5]}`, atau multipart dengan `files[]` (maks. 5). `@Nama` di teks juga dikenali; yang disebut ikut melihat tiket. |
| POST `/tickets/{id}/messages/{message}/feedback` | `tickets.view` | Kirim pesan diskusi sebagai feedback ke percakapan asal (catatan internal). |
| POST `/tickets/{id}/conclusion` | `tickets.view` | Kesimpulan untuk agen: `{"conclusion": "…", "resolve": true}` (`resolve` butuh `tickets.manage`). |
| GET `/ticket-messages/{id}/files/{index}` | `tickets.view` | Lampiran diskusi (privat). `?download=1` untuk mengunduh. |
| GET `/teams` | — | Tim Frontline & Back Office beserta anggotanya; `?type=frontline\|backoffice`. |
| GET `/teams/{id}` | — | Satu tim. |
| GET `/contacts` | `contacts.view` | Daftar kontak. Filter `q`, `label_id`, `wa_label_id`, `owner` (`me`/`none`/id), `channel_id`, `sort` (`recent`/`name`/`created`). |
| POST `/contacts` | `contacts.manage` | Tambah kontak: `name`, `phone`, `email`, `company`, `job_title`, `address`, `city`, `language`, `owner_id`, `label_ids`, `custom`. |
| GET `/contacts/{id}` | `contacts.view` | Profil lengkap: identitas, label Onix & WhatsApp, field kustom, statistik & aktivitas 12 bulan. |
| PATCH `/contacts/{id}` | `contacts.manage` | Ubah kontak (kolom yang dikirim saja). `label_ids` mengganti seluruh label. |
| POST `/contacts/{id}/labels` | `contacts.manage` | Pasang label Onix: `{"label_id": 3}` (juga boleh dengan `interactions.reply`). |
| DELETE `/contacts/{id}/labels/{label}` | `contacts.manage` | Lepas label Onix. |
| GET `/contacts/{id}/timeline` | `contacts.view` | Linimasa kontak, terbaru dulu (`before` = id terakhir). |
| GET `/contacts/{id}/tickets` | `contacts.view` | Tiket kontak beserta log statusnya (hanya yang boleh dilihat). |
| GET `/contacts/{id}/notes` | `contacts.view` | Catatan tim. |
| POST `/contacts/{id}/notes` | `contacts.view` | Tambah catatan: `{"body": "…"}`. |
| PATCH `/contacts/{id}/notes/{note}` | `contacts.view` | Ubah catatan (hanya penulisnya). |
| DELETE `/contacts/{id}/notes/{note}` | `contacts.view` | Hapus catatan (penulisnya atau `contacts.manage`). |
| GET `/labels` | `contacts.view` | Label Onix beserta jumlah kontaknya; `meta.colors` = warna yang tersedia. |
| GET `/custom-fields` | `contacts.view / interactions.view / tickets.view / leads.view` | Definisi field kustom per entitas: `?entity=contact\|interaction\|ticket\|lead\|client` (tanpa entity = semua). `meta.max_per_entity` = 30. |
| GET `/contact-fields` | `contacts.view` | Definisi field kustom kontak (sama dengan `/custom-fields?entity=contact`). |
| GET `/wa-label-rules` | `contacts.manage` | Aturan otomasi label WhatsApp (Pro): `{"label_name", "channel", "action", "value", "value_label", "active"}`. Juga boleh dengan `channels.manage`. |
| GET `/conversation-categories` | `interactions.view` | Kategori percakapan dalam urutan pohon (induk lalu subkategorinya): `{"id", "name", "color", "position", "parent_id", "depth", "path_text", "children_count"}`. Juga boleh dengan `contacts.view`. |
| GET `/kb/categories` | `kb.view` | Kategori knowledge base (urut posisi) dengan jumlah artikel terbit yang boleh dilihat pemanggil. |
| POST `/kb/categories` | `kb.manage` | Buat kategori: `{"name", "description", "position"}`. Nama unik per workspace. |
| PATCH `/kb/categories/{id}` | `kb.manage` | Ubah kategori (kolom yang tidak dikirim tetap). |
| DELETE `/kb/categories/{id}` | `kb.manage` | Hapus kategori; artikelnya menjadi tanpa kategori. |
| GET `/kb/articles` | `kb.view` | Daftar artikel berhalaman. Filter `q` (setiap kata wajib ada), `category_id` (id atau `none`), `visibility`, `status`, `team_id` (id tim = artikel khusus tim itu, `none` = untuk semua tim). Tanpa `kb.manage` hanya artikel terbit. Artikel khusus tim hanya untuk anggota tim itu (Owner, admin, "Bisa lihat semua data", dan API key melihat semua). Setiap artikel punya `teams` (kosong = semua tim). `meta.counts` = jumlah terbit & draf. |
| POST `/kb/articles` | `kb.manage` | Buat artikel: `{"title", "body", "category_id", "visibility", "status", "tags", "team_ids"}`. Bawaan Internal + Draf, untuk semua tim. Isi = Markdown sederhana. `team_ids` = tim yang boleh membaca; pengelola yang tidak melihat semua data hanya boleh memilih timnya sendiri (422). |
| GET `/kb/articles/{id}` | `kb.view` | Satu artikel lengkap dengan isi, `teams`, status pemrosesan pencarian makna (`embedding_status`, `chunk_count`, `embedded_at_text`), dan `can.insert` (artikel Publik terbit boleh disisipkan ke balasan). Artikel khusus tim lain → 404. |
| PATCH `/kb/articles/{id}` | `kb.manage` | Ubah artikel (kolom yang tidak dikirim tetap; `team_ids`: `[]` = semua tim). Terbit & judul/isi berubah → diproses ulang untuk pencarian makna. |
| DELETE `/kb/articles/{id}` | `kb.manage` | Hapus artikel. |
| GET `/kb/search` | `kb.view` | Cari artikel terbit. `mode=keyword` (bawaan, semua paket) atau `mode=semantic` (Pro/Custom, `ai.use`, 1 kuota AI per pencarian). Parameter `q`, `visibility`, `limit` (1–20). Hasil: `{"article", "match", "score", "snippet"}`; `meta.ai` = pemakaian AI bulan ini. |
| GET `/reports/summary` | `reports.view` | Kartu ringkasan untuk rentang `from`–`to` (YYYY-MM-DD menurut zona pembaca, inklusif; bawaan 30 hari terakhir, maks 366 hari) dan filter opsional `channel_id`, `team_id`: percakapan masuk, pesan masuk/keluar, respons pertama (rata-rata, median, % dalam target), waktu penyelesaian, belum dibalas sekarang, tiket. Rumus: [Rumus laporan](https://onix.sassly.ai/id/docs/laporan). |
| GET `/reports/daily` | `reports.view` | Satu baris per tanggal dalam rentang: `{"date", "conversations", "messages_in", "messages_out"}` (0 bila kosong). Parameter sama dengan ringkasan. |
| GET `/reports/hourly` | `reports.view` | 24 baris pesan masuk per jam 0–23 (zona pembaca): `{"hour", "messages_in"}`. |
| GET `/reports/breakdown` | `reports.view` | `type` = `channel`, `label`, `category`, atau `team`: baris per kelompok dengan jumlah percakapan masuk (per nomor juga pesan & rata-rata respons pertama; per tim juga tiket dibuat/selesai; per kategori urut pohon dengan `conversations` termasuk subkategori dan `conversations_direct`). |
| GET `/kpi` | `reports.view` | Kartu KPI per orang (Pro/Custom; Free → 402): `frontline` (respons pertama, balasan lanjutan, penyelesaian, Solved per hari kerja, kontak kembali, eskalasi, lewat target sekarang) dan `backoffice` (respons tiket, penyelesaian tepat waktu per prioritas, selesai per hari kerja, dibuka lagi, feedback, backlog). Waktu dalam menit/jam kerja; `from`/`to` (zona workspace, maks 93 hari), `team_id`, `channel_type` (`whatsapp`/`email`/`livechat`/`api`: hanya percakapan & tiket dari jenis channel itu, dengan target jenis itu — email, livechat, dan API punya target sendiri). Lihat [KPI & SLA](https://onix.sassly.ai/id/docs/kpi). |
| GET `/kpi/users/{id}` | — | KPI satu orang dengan `daily`, `breaches`, dan `targets` (beserta sumbernya). Diri sendiri selalu; orang lain butuh `reports.view`. |
| GET `/kpi/me` | — | KPI-mu hari ini & 7 hari terakhir (kartu "KPI saya" di Dashboard). |
| GET `/kpi/config` | `settings.workspace` | Target workspace, target tim & orang, daftar metrik (termasuk target email `email_first_response` & `email_reply` dalam jam, livechat `livechat_first_response` & `livechat_reply` dan channel API `api_first_response` & `api_reply` dalam menit), dan jam kerja workspace. |
| GET `/reports/agents` | `reports.view` | Produktivitas setiap anggota aktif: `{"user", "replies", "conversations", "first_responses", "avg_first_response_minutes", "tickets_resolved"}`. |
| POST `/interactions/{id}/summary` | `ai.use` | Ringkasan AI percakapan (Pro/Custom, 1 kuota AI): 3–6 poin, disimpan dan terlihat di `GET /interactions/{id}` → `ai_summary`. |
| GET `/templates` | `interactions.reply` | Template balasan. Dengan `interaction_id`, tiap template membawa `rendered` (variabel terisi). Filter `q`, `category`. |
| GET `/templates/{id}` | `interactions.reply` | Satu template. |
| GET `/templates/{id}/attachment` | `interactions.reply` | Lampiran template. |
| GET `/settings` | `settings.workspace` | Pengaturan General (zona waktu workspace, target respons, jam kerja `business_hours` + `open_now`, …) dan pilihannya di `meta.choices`. |
| GET `/members` | `settings.members` | Semua anggota termasuk Owner. |
| GET `/invitations` | `settings.members` | Undangan yang belum diterima, termasuk yang kedaluwarsa. |
| GET `/roles` | `settings.roles` | Semua role beserta permission dan jumlah pemakai (atau `settings.members`). |
| GET `/roles/{id}` | `settings.roles` | Satu role. |
| GET `/permissions` | `settings.roles` | Katalog permission per kelompok menu; `meta.grantable` = yang boleh diberikan pengguna ini. |
| GET `/audit-logs` | `settings.audit` | Audit log terbaru dulu. Filter: `category`, `user`, `from`, `to` (YYYY-MM-DD, zona pembaca), `q`. Bawaan 50 per halaman. |
| GET `/pipelines` | `leads.view / leads.manage` | Workflow pipeline yang terlihat (admin, "lihat semua data", API key, dan `pipeline.manage`: semua; selain itu workflow tim pemanggil): tahap (`kind` `new`/`progress`/`won`/`lost`, label, warna, `probability`) dan tim Back Office yang menangani. `meta` = alasan kalah, kategori biaya, palet warna. |
| GET `/pipelines/{id}` | `leads.view / leads.manage` | Satu workflow (pemegang `pipeline.manage` juga mendapat jumlah lead per tahap). |
| GET `/pipeline/settings` | `leads.view / leads.manage` | Alasan kalah, kategori biaya, palet warna tahap, dan batas tahap Proses. |
| GET `/pipeline/dashboard` | `leads.view / leads.manage` | Dashboard pipeline: `tiles` (terbuka, perkiraan, baru 7 hari, menang/kalah, win rate, rata-rata hari closing, follow-up lewat), `funnel`, `weekly` (8 minggu), `by_assignee`, `expenses`, `agenda` hari ini, `quota`. Saringan `pipeline_id`, `from`/`to` (bawaan bulan berjalan), `team_id`, `assignee_id`. Hanya lead yang boleh dilihat. |
| GET `/leads` | `leads.view / leads.manage` | Daftar lead (kepemilikan data: lead ber-PJ hanya untuk PJ-nya, tanpa PJ untuk tim lead; admin/API key semua). Saringan `pipeline_id`, `stage_id`, `status` (`open`/`won`/`lost`), `team_id`, `assignee` (`me`/`none`/id), `client_id`, `contact_id`, `q` (`LEAD-…`, judul, kontak, klien), `followup` (`overdue`/`today`/`upcoming`/`none`), `created_from`/`created_to`; `sort` & `dir`. `meta.totals` = jumlah, nilai prospek, nilai deal. |
| POST `/leads` | `leads.manage` | Buat lead (create_lead; sumber `api`): `pipeline_id`, `title`, kontak (`contact_id`, atau `contact` `{name, phone, email}` dicocokkan lewat nomor HP lalu email, atau `interaction_id`), `client_id`/`client_name`, `stage_id`, `team_id`, `assignee_id`, `value_estimate`, `budget`, `expected_close`, `followup_at`, `note`, `custom`. Kuota lead bulanan berlaku (402). |
| GET `/leads/board` | `leads.view / leads.manage` | Papan kanban satu workflow (`pipeline_id` wajib): tahap + kartu lead terbuka & Menang/Kalah bulan berjalan (maks 100 per tahap, `more`), `count`/`value` per tahap, dan `summary`. |
| GET `/leads/candidates` | `leads.view / leads.manage` | Anggota yang bisa menjadi PJ, dengan `team_ids` Back Office & `is_admin`. |
| GET `/leads/duplicates` | `leads.manage` | Lead terbuka kontak di workflow yang sama (`contact_id`, `pipeline_id`) — peringatan sebelum membuat lead. |
| GET `/leads/{id}` | `leads.view / leads.manage` | Detail lead: workflow & tahap, kontak, klien, percakapan asal, 30 aktivitas terbaru, checklist, jadwal, file, biaya, field kustom, dan `can`. |
| PATCH `/leads/{id}` | `leads.manage` | Ubah `title`, `client_id`/`client_name`, `value_estimate`, `value_won` (lead Menang), `budget`, `expected_close`, `followup_at`/`followup_note` (`null` = hapus), `team_id`, `assignee_id`, `lost_reason`/`lost_note` (lead Kalah), `custom`. Hasil `{"id", "visible": false}` bila pemanggil tidak lagi boleh melihatnya. |
| POST `/leads/{id}/move` | `leads.manage` | Pindah tahap: `{"stage_id"}`; ke Menang wajib `value_won` (bawaan nilai prospek), ke Kalah wajib `lost_reason` dari daftar (+ `lost_note`). Dari Menang/Kalah ke tahap Baru/Proses = buka lagi. |
| GET `/leads/{id}/activities` | `leads.view / leads.manage` | Linimasa lead berhalaman (`before` = id terakhir, `kind=note` untuk catatan saja). |
| POST `/leads/{id}/activities` | `leads.manage` | Catat aktivitas: `{"kind": "call\|chat\|email\|meeting\|visit\|note\|other", "body", "happened_at", "duration_minutes"}`. `@Nama` memberi notifikasi. |
| GET `/lead-files/{id}` | `leads.view / leads.manage` | File lead (privat). `?download=1` untuk mengunduh. |
| GET `/lead-expenses/{id}/receipt` | `leads.view / leads.manage` | Bukti biaya (privat). `?download=1` untuk mengunduh. |
| GET `/interactions/{id}/leads` | `interactions.view` | Tab Leads percakapan: SEMUA lead dari percakapan ini (ringkas, `can_open`) + lead terbuka lain kontaknya yang boleh dilihat. |
| GET `/contacts/{id}/leads` | `contacts.view` | Lead kontak yang boleh dilihat (kosong tanpa akses Pipeline). |
| GET `/clients` | `contacts.view / leads.view` | Master klien (tidak dibatasi tim): `q`, `industry`; setiap klien dengan `contacts_count` dan ringkasan lead yang boleh dilihat. `meta.industries` untuk saringan. |
| GET `/clients/search` | `contacts.view / leads.view` | Cari klien untuk pilihan di form: `?q=`. |
| POST `/clients` | `clients.manage` | Tambah klien: `{"name", "industry", "phone", "email", "website", "address", "city", "notes", "custom"}`. Nama unik per workspace. |
| GET `/clients/{id}` | `contacts.view / leads.view` | Profil klien, kontak tertaut, lead yang boleh dilihat, dan ringkasan deal. |
| PATCH `/clients/{id}` | `clients.manage` | Ubah klien. |
| GET `/segments` | `broadcasts.view / broadcasts.manage` | Segmen kontak: aturan, ringkasan, `last_count`, jumlah broadcast yang memakainya. |
| GET `/segments/fields` | `broadcasts.view / broadcasts.manage` | Katalog field, operator, & pilihan nilai untuk aturan segmen (termasuk field kustom `cf:{kunci}`). |
| POST `/segments/preview` | `broadcasts.view / broadcasts.manage` | Pratinjau aturan tanpa menyimpan: `{"match": "all\|any", "rules": [{"field", "op", "value", "value2"}]}` → jumlah, punya nomor, berhenti berlangganan, contoh. |
| GET `/segments/{id}` | `broadcasts.view / broadcasts.manage` | Detail segmen + hitung ulang isinya (`preview`). |
| GET `/broadcasts` | `broadcasts.view / broadcasts.manage` | Daftar broadcast (`status`, `channel_id`, `q`) dengan angka per status (`stats`). |
| GET `/broadcasts/options` | `broadcasts.view / broadcasts.manage` | Nomor (izin broadcast), segmen, pengaturan bawaan & batas aman, kuota, variabel pesan. |
| GET `/broadcasts/dashboard` | `broadcasts.view / broadcasts.manage` | Angka periode (`from`, `to`, `channel_id`; maks 92 hari): corong, harian, per jam, alasan, pemakaian nomor hari ini, broadcast terbaru & terbaik. |
| GET `/broadcasts/{id}` | `broadcasts.view / broadcasts.manage` | Detail broadcast: audiens, pesan, lampiran, pengaturan, angka, alasan dilewati, persetujuan risiko, `wait`, `can`. |
| GET `/broadcasts/{id}/recipients` | `broadcasts.view / broadcasts.manage` | Penerima & status per penerima (`status` bertingkat, `q`), termasuk isi pesan yang terkirim. |
| GET `/broadcasts/{id}/export` | `broadcasts.view / broadcasts.manage` | Laporan per penerima `?format=xlsx\|csv`. |
| GET `/broadcasts/{id}/media` | `broadcasts.view / broadcasts.manage` | Lampiran broadcast (privat). |
| POST `/broadcasts/{id}/estimate` | `broadcasts.view / broadcasts.manage` | Perkiraan penerima (alasan dilewati), kuota, waktu selesai, dan tingkat risiko. |
| POST `/broadcasts/{id}/preview` | `broadcasts.view / broadcasts.manage` | Teks jadi untuk maks 5 kontak contoh (`variant`, opsional `text`/`footer`). |

### Khusus aplikasi Onix

Endpoint berikut hanya menerima sesi aplikasi (cookie + `X-CSRF-Token`). Dengan API key dibalas `403 forbidden`.

| Endpoint | Permission | Keterangan |
| --- | --- | --- |
| POST `/wa-label-rules` | `contacts.manage` | Buat aturan label WhatsApp (Pro): `action` = `contact_label` (value = id label kontak), `assign_team` (id tim Frontline), atau `solve`. Free → 402. |
| PATCH `/wa-label-rules/{id}` | `contacts.manage` | Ubah aturan (termasuk `active`). |
| DELETE `/wa-label-rules/{id}` | `contacts.manage` | Hapus aturan. |
| POST `/conversation-categories` | `contacts.manage` | Buat kategori percakapan: `{"name", "color", "parent_id"}` — sampai 3 tingkat dan 200 kategori; nama unik di antara saudara. |
| PATCH `/conversation-categories/{id}` | `contacts.manage` | Ubah nama, warna, urutan, atau induk kategori (`parent_id`, `null` = kategori utama; tidak ke dirinya/subkategorinya, maks 3 tingkat). |
| DELETE `/conversation-categories/{id}` | `contacts.manage` | Hapus kategori; subkategori & percakapannya pindah ke induknya (tanpa kategori bila kategori utama). |
| PUT `/kpi/config` | `settings.workspace` | Simpan target workspace (`targets`) dan/atau target tim & orang (`overrides`, mengganti semuanya). Jam kerja kini diatur lewat `PATCH /settings` (`business_hours` di sini tetap diterima). |
| GET `/kpi/export` | `reports.view` | CSV KPI: `type` = `summary`, `conversations`, `replies`, atau `tickets`; `user_id` untuk satu orang (boleh untuk dirimu sendiri tanpa `reports.view`); `channel_type` seperti di atas. Data mentah punya kolom `channel_type` di akhir. |
| POST `/kb/articles/{id}/embed` | `kb.manage` | Proses (ulang) artikel terbit untuk pencarian makna (Pro/Custom) — tombol Proses sekarang / Proses ulang / Coba lagi. |
| GET `/reports/export` | `reports.view` | Unduh CSV (UTF-8 + BOM): `type` = `summary`, `daily`, `channel`, `label`, `category`, `agents`, atau `tickets`, dengan rentang & filter yang sama seperti laporan JSON. |
| PATCH `/me` | — | Bahasa & zona waktu pribadi: `{"locale": "id", "timezone": "Asia/Makassar"}` (`""` = ikut workspace). |
| POST `/me/preferences` | — | Preferensi pribadi, mis. status tur, sidebar, dan `livechat_alert` (`0`/`1`: bunyi & judul tab berkedip saat chat livechat baru menunggu). |
| GET `/me/invitations` | — | Undangan berlaku untuk email akun ini. |
| POST `/me/invitations/{id}/accept` | — | Terima undangan; workspace itu menjadi workspace aktif. |
| POST `/me/workspace` | — | Pindah workspace aktif: `{"workspace_id": 3}`. |
| GET `/workspaces` | — | Workspace tempat pengguna menjadi anggota aktif. |
| POST `/workspaces` | — | Buat workspace: `{"name": "…", "timezone": "Asia/Jakarta"}`. Akun Free/Pro memiliki satu workspace; pelanggan Custom sesuai jatah kontraknya (409). `meta.create` di `GET /workspaces` menunjukkan boleh/tidaknya. |
| GET `/workspace/transfer` | — | Pindah kepemilikan (khusus Owner): permintaan yang menunggu + calon penerima dan kelayakannya. |
| POST `/workspace/transfer` | — | Minta pindah kepemilikan: `to_user_id`, `confirm_name` (nama workspace), `stay`, `stay_role_id`, `stay_team_ids`. |
| DELETE `/workspace/transfer` | — | Batalkan permintaan yang menunggu. |
| GET `/me/transfers` | — | Permintaan pindah kepemilikan yang menunggu jawabanku. |
| GET `/me/transfers/{id}` | — | Detail permintaan (`can_accept`, `becomes_free`). |
| POST `/me/transfers/{id}/accept` | — | Terima: aku menjadi Owner; workspace itu menjadi workspace aktif. |
| POST `/me/transfers/{id}/decline` | — | Tolak permintaan. |
| PATCH `/workspace` | `settings.workspace` | Ganti nama workspace. |
| POST `/workspace/leave` | — | Keluar dari workspace aktif (bukan Owner). |
| PATCH `/settings` | `settings.workspace` | Ubah sebagian pengaturan General, mis. `{"timezone": "Asia/Jakarta"}` atau jam kerja workspace (semua paket) `{"business_hours": {"enabled": true, "days": [{"day": 1, "open": true, "from": "08:00", "to": "17:00"}, …]}}` — hari 1 = Senin. |
| GET `/notifications` | — | 20 notifikasi terbaru dan jumlah yang belum dibaca. |
| POST `/notifications/read` | — | Tandai satu (`{"id": 12}`) atau semua notifikasi dibaca. |
| POST `/realtime/token` | — | Token WebSocket (Centrifugo) untuk pembaruan langsung; `{"channels": ["conv:12", "ticket:5"]}` untuk ikut kanal percakapan/tiket. |
| POST `/interactions/{id}/read` | `interactions.view` | Tandai dibaca; tanda biru dikirim ke pelanggan. |
| POST `/interactions/{id}/typing` | `interactions.reply` | Indikator mengetik: `{"typing": true}`. |
| POST `/interactions/{id}/pickup` | `interactions.reply` | Ambil chat livechat yang belum diambil (atomik): yang kalah cepat mendapat 409 "Sudah diambil oleh {nama}". Pengunjung melihat "{nama} bergabung ke chat". |
| POST `/interactions/{id}/history` | `interactions.view` | Salin riwayat lama dari HP untuk percakapan ini. |
| POST `/channels` | `channels.manage` | Tambah nomor WhatsApp: `{"name": "CS Toko"}` (kuota channel dicek). `grant_selected_members: true` = anggota yang aksesnya "channel tertentu" ikut diberi akses (juga di `POST /channels/email`, `/livechat`, `/api`). |
| GET `/channels/{id}` | `channels.manage` | Detail channel. |
| PATCH `/channels/{id}` | `channels.manage` | Semua jenis channel: `name`, jam kerja `hours` (objek seperti `business_hours`, atau `null` = ikuti jam kerja workspace), balasan otomatis `auto_reply` (`greeting_on`, `greeting`, `away_on`, `away`). WhatsApp: `groups`, `reject_calls`, `call_reply`, `agent_initial` (tambahkan inisial agen di akhir balasan). Pengaturan email/livechat/API lainnya lewat `PATCH /channels/{id}/email`, `/livechat`, `/api`. |
| DELETE `/channels/{id}` | `channels.manage` | Hapus channel (diarsipkan bila sudah punya percakapan). |
| GET `/channels/{id}/qr` | `channels.manage` | QR untuk scan (data URL PNG) + umur dalam detik. |
| POST `/channels/{id}/pair` | `channels.manage` | Kode pairing: `{"phone": "0812…"}`. |
| GET `/channels/{id}/status` | `channels.manage` | Status terbaru nomor. |
| POST `/channels/{id}/logout` | `channels.manage` | Logout nomor dari Onix. |
| POST `/channels/{id}/reconnect` | `channels.manage` | Sambung ulang koneksi. |
| POST `/channels/{id}/history` | `channels.manage` | Impor riwayat chat: `{"days": 30}` (7/30/90 hari). |
| GET `/channels/{id}/labels` | `channels.manage` | Label WhatsApp Business (dibaca dari HP). |
| POST `/channels/email/detect` | `channels.manage` | Tebak penyedia dari alamat: `{"address": "cs@tokoanda.com"}` → `provider`, saran server IMAP/SMTP, dan petunjuk (mis. butuh App Password; Microsoft 365 belum didukung). Hanya sesi login. |
| POST `/channels/email/test` | `channels.manage` | Tes login IMAP & SMTP tanpa mengirim email: `address`, `password`, opsional `username`, `imap_host`/`imap_port`/`imap_security` (`ssl`/`starttls`), `smtp_host`/`smtp_port`/`smtp_security`, `smtp_username`/`smtp_password`. Hasil: folder yang bisa dipilih, folder Terkirim terdeteksi. Kesalahan dijelaskan per kolom (422). |
| POST `/channels/email` | `channels.manage` | Sambungkan alamat email (kuota channel dicek; satu alamat hanya di satu workspace → 409): isian tes di atas + `name`, `folders` (selain Inbox), `sent_folder`, `read_sent`, `save_sent`, `history_days` (0/3/7/30, bawaan 7), `sender_name`, `sender_with_agent`, `signature_html`, `ignore`. Password disimpan terenkripsi dan tidak pernah dikembalikan. |
| PATCH `/channels/{id}/email` | `channels.manage` | Ubah pengaturan email (kolom sama dengan di atas, kecuali `history_days`). Perubahan server, username, password, atau folder dites ulang sebelum disimpan. |
| GET `/channels/{id}/email/folders` | `channels.manage` | Daftar folder kotak surat (login ulang) dengan tanda `selected`, plus folder Terkirim. |
| POST `/channels/{id}/sync` | `channels.manage` | Periksa kotak surat sekarang (jeda setelah login gagal diabaikan). Hasil `{"sync", "channel"}`. |
| POST `/channels/{id}/icon` | `channels.manage` | Unggah ikon/logo channel (multipart `icon`: PNG/JPG/WebP persegi, maks 512 KB). Livechat: logo yang sama tampil di kepala widget & peluncur. |
| DELETE `/channels/{id}/icon` | `channels.manage` | Kembali ke ikon bawaan jenis channel. |
| POST `/channels/livechat` | `channels.manage` | Buat channel livechat (semua paket, satu slot channel): `{"name": "Chat Website", "title", "color", "grant_selected_members"}` → channel dengan `livechat.key` & `livechat.embed_code`. Lihat [Livechat](https://onix.sassly.ai/id/docs/livechat). |
| PATCH `/channels/{id}/livechat` | `channels.manage` | Pengaturan widget: `title`, `subtitle`, `color`, `font`, `radius`, `position`, `launcher_icon`, `intro`, `consent`, `start_label`, `attachments` (`off`/`images`/`files`), `email_fallback`, `allowed_domains`; juga `name`, `hours`, `auto_reply`. |
| POST `/channels/{id}/livechat/key` | `channels.manage` | Ganti kunci publik; kode sematan lama berhenti bekerja. |
| POST `/channels/api` | `channels.manage` | Buat channel API (Pro/Custom, satu slot channel): `{"name", "webhook_url", "events", "retries", "grant_selected_members"}` → `credentials.key` (hanya tampil sekali) & `credentials.webhook_secret`. Lihat [Channel API](https://onix.sassly.ai/id/docs/api-channel). |
| GET `/channels/{id}/api` | `channels.manage` | Pengaturan, endpoint pesan masuk, kunci aktif (awalan & terakhir dipakai), dan 50 pengiriman webhook terbaru. |
| PATCH `/channels/{id}/api` | `channels.manage` | `webhook_url` (https, host publik), `events` (`message.created` selalu; `conversation.updated` opsional), `retries` (0–5); juga `name`, `hours`, `auto_reply`. |
| POST `/channels/{id}/api/key` | `channels.manage` | Ganti kunci channel; kunci lama langsung ditolak (401). |
| GET `/channels/{id}/api/secret` | `channels.manage` | Lihat secret webhook. |
| POST `/channels/{id}/api/secret` | `channels.manage` | Ganti secret webhook. |
| POST `/channels/{id}/api/test-webhook` | `channels.manage` | Kirim event `ping` sekarang: `{"status", "http_status", "duration_ms", "error"}`. |
| POST `/channels/{id}/api/test-message` | `channels.manage` | Simulasikan pesan masuk dari sistem Anda (tampil di Interaction). |
| POST `/templates` | `templates.manage` | Buat template: `name`, `shortcut`, `category`, `body` (JSON atau multipart dengan `file`). |
| PATCH `/templates/{id}` | `templates.manage` | Ubah template. |
| DELETE `/templates/{id}` | `templates.manage` | Hapus template. |
| POST `/templates/{id}/attachment` | `templates.manage` | Pasang/ganti lampiran (multipart `file`). |
| DELETE `/templates/{id}/attachment` | `templates.manage` | Lepas lampiran. |
| GET `/contacts/export` | `contacts.manage` | Ekspor kontak ke CSV (filter sama dengan daftar). |
| POST `/contacts/import` | `contacts.manage` | Impor CSV (multipart `file`): hasil `created`, `updated`, `skipped`, `errors`. |
| DELETE `/contacts/{id}` | `contacts.manage` | Hapus kontak beserta percakapan, pesan, media, catatan, dan linimasanya (tiket tetap ada tanpa tautan). |
| POST `/contacts/{id}/merge` | `contacts.manage` | Gabung kontak ganda: `{"source_id": 9}` dilebur ke kontak {id}. |
| POST `/labels` | `contacts.manage` | Buat label: `{"name": "VIP", "color": "orange"}`. |
| PATCH `/labels/{id}` | `contacts.manage` | Ubah nama, warna, atau urutan label. |
| DELETE `/labels/{id}` | `contacts.manage` | Hapus label (dilepas dari semua kontak). |
| POST `/custom-fields` | `contacts.manage` | Buat field kustom kontak, interaction, atau tiket: `{"entity": "interaction", "label": "Nomor invoice", "type": "text"}`. Kunci unik per entitas. |
| PATCH `/custom-fields/{id}` | `contacts.manage` | Ubah nama, pilihan, atau urutan field (entitas, jenis, & kunci tetap). |
| DELETE `/custom-fields/{id}` | `contacts.manage` | Hapus field kustom; nilainya tidak lagi tampil. |
| POST `/contact-fields` | `contacts.manage` | Buat field kustom kontak: `{"label": "Ukuran", "type": "select", "options": ["S", "M", "L"]}`. |
| PATCH `/contact-fields/{id}` | `contacts.manage` | Ubah nama, pilihan, atau urutan field. |
| DELETE `/contact-fields/{id}` | `contacts.manage` | Hapus field kustom. |
| PATCH `/members/{id}` | `settings.members` | Ubah role, `team_ids`, `view_all`, akses channel, atau status (`active`/`disabled`). Role bukan admin wajib minimal satu tim. `reply_initial` (maks 20 karakter, kosong = otomatis dari nama, mis. `^BS`) boleh diatur untuk semua anggota termasuk Owner. |
| POST `/invitations` | `settings.members` | Undang anggota: `email`, `role_id`, `team_ids`, `view_all`, `channel_access`, `channel_ids`. |
| POST `/teams` | `settings.members` | Buat tim: `{"name": "Gudang", "type": "backoffice", "member_ids": [5, 7]}`. |
| PATCH `/teams/{id}` | `settings.members` | Ubah nama, deskripsi, atau anggota tim (`member_ids` = daftar lengkap). Jenis tidak bisa diubah. |
| DELETE `/teams/{id}` | `settings.members` | Hapus tim; bila masih punya anggota/data (termasuk artikel KB khusus tim) wajib `?replacement_id=` tim sejenis. |
| POST `/invitations/{id}/resend` | `settings.members` | Kirim ulang undangan dengan tautan baru. |
| DELETE `/invitations/{id}` | `settings.members` | Batalkan undangan. |
| POST `/roles` | `settings.roles` | Buat role: `name`, `permissions`, dan `is_admin` (hanya Owner/admin). |
| PATCH `/roles/{id}` | `settings.roles` | Ubah nama, permission, dan tanda `is_admin` role (kecuali Owner). |
| DELETE `/roles/{id}` | `settings.roles` | Hapus role yang tidak dipakai siapa pun. |
| POST `/roles/{id}/duplicate` | `settings.roles` | Salin role. |
| GET `/api-keys` | `settings.api` | API key aktif, tanpa nilai rahasianya. |
| POST `/api-keys` | `settings.api` | Buat key: `{"name": "…", "scope": "read"}`. Nilai utuh hanya dikembalikan sekali. |
| DELETE `/api-keys/{id}` | `settings.api` | Cabut key. |
| GET `/billing` | `settings.billing` | Paket, pemakaian, invoice, dan profil tagihan. |
| POST `/billing/upgrade` | `settings.billing` | Terbitkan/pakai invoice upgrade lalu buat checkout Sassly Pay. |
| GET `/billing/invoices/{number}` | `settings.billing` | Detail invoice beserta riwayat percobaan bayar. |
| POST `/billing/invoices/{number}/pay` | `settings.billing` | Alamat checkout Sassly Pay (percobaan yang masih berlaku dipakai ulang). |
| PATCH `/billing/profile` | `settings.billing` | Profil tagihan untuk invoice berikutnya. |
| POST `/billing/downgrade-choice` | `settings.billing` | Pilih channel & anggota yang tetap aktif setelah turun ke Free. |
| GET `/billing/return` | `settings.billing` | Status pembayaran setelah kembali dari checkout (dicek ke Sassly Pay di server). |
| POST `/pipelines` | `pipeline.manage` | Buat workflow: `{"name", "description", "team_ids"}` (tim Back Office); tanpa `stages` → tahap bawaan. |
| PATCH `/pipelines/{id}` | `pipeline.manage` | Ubah nama, deskripsi, tim, urutan; `{"archived": false}` memulihkan. |
| PUT `/pipelines/{id}/stages` | `pipeline.manage` | Simpan susunan tahap lengkap: satu Baru, 0–10 Proses, satu Menang, satu Kalah (label, warna, peluang %). |
| DELETE `/pipelines/{id}` | `pipeline.manage` | Hapus workflow tanpa lead; yang masih punya lead diarsipkan. |
| PUT `/pipeline/settings` | `pipeline.manage` | Simpan `lost_reasons` & `expense_categories`. |
| GET `/pipeline/export` | `reports.view` | CSV raw data: `type` = `leads` (semua kolom + field kustom), `activities`, atau `expenses`; saringan `pipeline_id`, `status`, `from`/`to`. |
| GET `/leads/import/template` | `leads.manage` | Template impor `?format=xlsx\|csv`. |
| POST `/leads/import/preview` | `leads.manage` | Baca file Excel/CSV (multipart `file`): kolom, contoh isi, saran pemetaan, token 1 jam. |
| POST `/leads/import` | `leads.manage` | Periksa (`dry_run`) atau jalankan impor dengan pemetaan kolom (kolom tanpa padanan → field kustom baru). |
| POST `/leads/bulk` | `leads.manage` | Aksi massal (maks 100): `move`, `assign`, `followup`. |
| DELETE `/leads/{id}` | `leads.manage` | Hapus lead (admin, PJ, atau pembuat); kuota tidak kembali. |
| PATCH `/lead-activities/{id}` | `leads.manage` | Ubah aktivitas manual (penulis atau admin). |
| DELETE `/lead-activities/{id}` | `leads.manage` | Hapus aktivitas manual (penulis atau admin). |
| POST `/leads/{id}/files` | `leads.manage` | Unggah file lead (multipart `file`). |
| DELETE `/lead-files/{id}` | `leads.manage` | Hapus file (pengunggah, PJ, atau admin). |
| POST `/leads/{id}/checklist` | `leads.manage` | Tambah butir checklist `{"text"}`; `PATCH /lead-checklist/{id}` (`text`, `done`, `position`) dan `DELETE /lead-checklist/{id}`. |
| PATCH `/lead-checklist/{id}` | `leads.manage` | Ubah / centang butir checklist. |
| DELETE `/lead-checklist/{id}` | `leads.manage` | Hapus butir checklist. |
| POST `/leads/{id}/events` | `leads.manage` | Tambah jadwal: `{"kind", "title", "starts_at", "ends_at", "location", "notes", "remind_minutes"}`. |
| PATCH `/lead-events/{id}` | `leads.manage` | Ubah jadwal (mengubah waktu mulai mengatur ulang pengingat). |
| DELETE `/lead-events/{id}` | `leads.manage` | Hapus jadwal. |
| POST `/leads/{id}/expenses` | `leads.manage` | Catat biaya (multipart): `spent_on`, `category`, `amount`, `note`, `spent_by`, `receipt` (foto/PDF). |
| PATCH `/lead-expenses/{id}` | `leads.manage` | Ubah biaya (pencatat, pembayar, atau admin). |
| DELETE `/lead-expenses/{id}` | `leads.manage` | Hapus biaya beserta buktinya. |
| DELETE `/clients/{id}` | `clients.manage` | Hapus klien; kontak & lead tetap ada tanpa tautan klien. |
| POST `/clients/{id}/contacts` | `clients.manage` | Tautkan (`{"contact_id", "link": true}`) atau lepas kontak; lead kontak yang belum berklien ikut tertaut. |
| POST `/contacts/{id}/broadcast-opt-out` | `contacts.manage / broadcasts.manage` | Berhenti (`{"opt_out": true}`) atau kembali berlangganan broadcast. |
| POST `/segments` | `broadcasts.manage` | Buat segmen: `{"name", "description", "match", "rules"}` (maks 15 aturan, nama unik). |
| PATCH `/segments/{id}` | `broadcasts.manage` | Ubah segmen. |
| DELETE `/segments/{id}` | `broadcasts.manage` | Hapus segmen (ditolak bila dipakai broadcast yang terjadwal/berjalan/dijeda). |
| GET `/segments/{id}/export` | `broadcasts.manage` | Unduh isi segmen (CSV). |
| POST `/broadcasts` | `broadcasts.manage` | Buat draf (Pro/Custom): `{"name", "channel_id", "audience", "messages", "settings"}`. |
| PATCH `/broadcasts/{id}` | `broadcasts.manage` | Ubah draf (bagian yang dikirim saja). |
| DELETE `/broadcasts/{id}` | `broadcasts.manage` | Hapus draf, broadcast selesai, atau dibatalkan. |
| POST `/broadcasts/{id}/media` | `broadcasts.manage` | Pasang lampiran (multipart `file`: gambar, video, dokumen); `DELETE` untuk melepas. |
| POST `/broadcasts/{id}/test` | `broadcasts.manage` | Kirim uji ke satu nomor `{"phone", "variant"}` (tidak memakai kuota, dibatasi per jam). |
| POST `/broadcasts/{id}/schedule` | `broadcasts.manage` | Mulai sekarang/jadwalkan dengan `{"risk_ack": true}`; daftar penerima dikunci. |
| POST `/broadcasts/{id}/pause` | `broadcasts.manage` | Jeda. Juga `/resume`, `/cancel`, `/retry` (kirim ulang yang gagal), `/unschedule` (kembali ke draf), `/duplicate`. |

### Publik

| Endpoint | Permission | Keterangan |
| --- | --- | --- |
| GET `/public/config` | — | Harga & kuota paket, pajak, dan aturan retensi (mengikuti `X-Locale`). |
| GET `/public/demo-livechat` | — | Kunci livechat data contoh untuk halaman [/demo/livechat](https://onix.sassly.ai/demo/livechat) (`null` bila belum ada). |
| POST `/plan-requests` | — | Permintaan paket Custom: `name`, `email`, `message`, opsional `company`, `phone`, `channels`, `agents`. |
| GET `/invite/{token}` | — | Ringkasan undangan dari tautan email. |

### Widget livechat (publik)

Dipanggil widget livechat (pemuat `livechat.js` & iframe Onix) tanpa login. `{key}` = kunci publik channel. Selain `config` dan `logo`, semua memakai token pengunjung di `Authorization: Bearer`. Penjelasan lengkap: [Livechat](https://onix.sassly.ai/id/docs/livechat).

| Endpoint | Akses | Keterangan |
| --- | --- | --- |
| GET `/livechat/{key}/config` | — | Tampilan & teks widget, status jam kerja, token formulir (CORS `*`). |
| GET `/livechat/{key}/logo` | — | Logo widget. |
| POST `/livechat/{key}/visitors` | — | Mulai chat: `name`, `email`, `phone` (wajib), `page_url`, `locale`, `form_token` → `token` pengunjung. |
| GET `/livechat/{key}/conversation` | — | Percakapan pengunjung + pesan sesudah `?after={id}`, posisi antrian, agen. |
| POST `/livechat/{key}/messages` | — | Kirim pesan: `text`, `client_id` (kirim ulang aman), opsional lampiran multipart `file`. |
| GET `/livechat/{key}/files/{id}` | — | Lampiran di percakapan pengunjung itu saja. |
| POST `/livechat/{key}/realtime` | — | Token WebSocket kanal `visitor:{sesi}`. |
| POST `/livechat/{key}/typing` | — | Pengunjung sedang mengetik. |
| POST `/livechat/{key}/seen` | — | Widget terbuka & pesan sampai `message_id` sudah dilihat. |

### Channel API (kunci channel)

Untuk sistem Anda yang mengirim pesan pelanggan ke Onix lewat channel API, dengan kunci channel `onx_ch_…` di `Authorization: Bearer` (bukan API key workspace). Balasan agen dikirim ke webhook Anda. Panduan, contoh payload, dan verifikasi tanda tangan: [Channel API](https://onix.sassly.ai/id/docs/api-channel).

| Endpoint | Akses | Keterangan |
| --- | --- | --- |
| POST `/channel-api/messages` | — | Pesan masuk: `{"conversation_id", "contact": {"id", "name", "email", "phone"}, "message": {"id", "text", "attachments", "sent_at"}}`. `message.id` unik → kirim ulang aman. |
| POST `/channel-api/messages/{id}/status` | — | Tandai balasan agen `delivered`/`read`. |
| GET `/channel-api/files/{token}` | — | Lampiran balasan agen (tautan bertanda tangan di webhook, 7 hari). |

## Contoh

### Siapa pemilik key ini

```
curl -s "https://onix.sassly.ai/api/v1/me" \
  -H "Authorization: Bearer $ONIX_API_KEY"
```

```
{
  "data": {
    "user": {"id": 12, "name": "Budi Santoso", "email": "budi@tokobudi.id", "avatar_url": null, "locale": "id", "timezone": null, "last_login_at": "2026-10-08T02:00:00Z"},
    "via": "key",
    "timezone": "Asia/Jakarta",
    "workspace": {
      "id": 3, "name": "Toko Budi", "slug": "toko-budi", "plan": "pro", "pro_until": "2026-11-08T03:15:00Z", "in_grace": false, "timezone": "Asia/Jakarta",
      "limits": {"channels": 4, "users": 10, "tickets_per_month": 200, "replies_per_day": 0, "ai_per_month": 2000, "ai": true, "api": true},
      "created_at": "2026-10-08T02:00:00Z"
    },
    "role": {"id": 1, "name": "Owner"},
    "is_owner": true,
    "is_platform_admin": false,
    "permissions": ["dashboard.view", "interactions.view", "interactions.reply", "interactions.assign", "templates.manage", "channels.manage", "settings.members", "settings.roles", "settings.billing", "settings.audit", "settings.workspace", "settings.api"],
    "channel_ids": null,
    "realtime": true
  }
}
```

Pada `limits`, nilai `0` berarti tanpa batas. `channel_ids` bernilai `null` berarti akses ke semua channel. `timezone` adalah zona efektif (zona pribadi pengguna, atau zona workspace).

### Membalas pelanggan WhatsApp

Cari percakapan yang belum dibalas, lalu kirim balasan (butuh key ber-scope `write`). Pesan dikirim lewat nomor WhatsApp percakapan itu dan tercatat atas nama key.

```
curl -s "https://onix.sassly.ai/api/v1/interactions?tab=unreplied&per_page=5" \
  -H "Authorization: Bearer $ONIX_API_KEY"
```

```
curl -s -X POST "https://onix.sassly.ai/api/v1/interactions/42/messages" \
  -H "Authorization: Bearer $ONIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Halo kak, pesanan sudah kami kirim hari ini 🙏"}'
```

```
{
  "data": {
    "id": 981, "interaction_id": 42, "direction": "out", "type": "text", "body": "Halo kak, pesanan sudah kami kirim hari ini 🙏",
    "media": null, "reply_to": null, "sender": {"name": "API · CRM", "user_id": 12, "jid": null},
    "status": "sent", "error": null, "reactions": [], "sent_at": "2026-10-09T03:15:00Z", "created_at": "2026-10-09T03:15:00Z"
  },
  "meta": {"replies": {"used": 1, "limit": 0}}
}
```

Pesan yang ditolak WhatsApp tetap tersimpan dengan `"status": "failed"` dan alasan di `error`; kirim ulang lewat `/retry`. Maksimal 30 pesan per menit per nomor (`429` bila lewat). Lampiran: kirim `multipart/form-data` dengan field `file` (maks. 16 MB).

### Audit log anggota sejak awal bulan

```
curl -s "https://onix.sassly.ai/api/v1/audit-logs?category=anggota&from=2026-10-01&per_page=20" \
  -H "Authorization: Bearer $ONIX_API_KEY"
```

```
{
  "data": [
    {
      "id": 41, "action": "member.invited", "label": "Mengundang anggota", "category": "anggota", "icon": "user-plus",
      "target": "rina@tokobudi.id", "details": "Role: Agent",
      "user": {"id": 12, "name": "Budi Santoso", "avatar_url": null},
      "ip": "203.0.113.10", "created_at": "2026-10-08T03:20:00Z"
    }
  ],
  "meta": {"page": 1, "per_page": 20, "total": 1, "pages": 1, "categories": [{"key": "anggota", "label": "Anggota & undangan"}, "…"], "users": [{"id": 12, "name": "Budi Santoso"}]}
}
```

### Contoh JavaScript (Node 18+) dan PHP

```
const res = await fetch("https://onix.sassly.ai/api/v1/members", {
  headers: { Authorization: `Bearer ${process.env.ONIX_API_KEY}` },
});
const { data, error } = await res.json();
if (error) throw new Error(`${error.code}: ${error.message}`);
```

```
$ch = curl_init('https://onix.sassly.ai/api/v1/roles');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('ONIX_API_KEY')],
]);
$body = json_decode(curl_exec($ch), true);
```

## Event real-time

Aplikasi menerima event lewat WebSocket (Centrifugo) di kanal `ws:{workspace_id}` dan `user:{user_id}`. Isi setiap event: `{"event": "…", "data": {…}, "at": "…Z"}`.

| Event | Kanal | Kapan |
| --- | --- | --- |
| `notification.created` | `user:{id}` | Notifikasi baru untuk pengguna. |
| `workspace.updated` | `ws:{id}` | Nama workspace berubah. |
| `settings.updated` | `ws:{id}` | Pengaturan General berubah (mis. zona waktu). |
| `members.changed` | `ws:{id}` | Anggota/undangan berubah. |
| `teams.changed` | `ws:{id}` | Tim atau anggota tim berubah. |
| `billing.updated` | `ws:{id}` | Paket berubah (Pro aktif, turun ke Free, diatur admin). |
| `inbox.changed` | `ws:{id}` | Ada perubahan di inbox: `{"interaction_id", "channel_id", "reason"}` tanpa isi pesan — muat ulang daftar lewat API (akses channel tetap dicek). |
| `channel.updated` | `ws:{id}` | Status/pengaturan channel berubah, termasuk progres impor riwayat. |
| `labels.changed` | `ws:{id}` | Label WhatsApp Business sebuah channel berubah. |
| `message.created` / `message.updated` | `conv:{id}` | Pesan baru atau berubah (status kirim, reaksi, edit, tarik, media selesai diunduh). Isi = resource pesan. |
| `interaction.updated` | `conv:{id}` | Status, penugasan, atau ringkasan percakapan berubah. |
| `typing` | `conv:{id}` | Pelanggan (WhatsApp/livechat) atau agen lain sedang mengetik. |
| `visitor.online` | `conv:{id}` | Pengunjung livechat sedang membuka widget (dikirim widget ±30 detik sekali). |
| `tickets.changed` | `ws:{id}` | Tiket dibuat/berubah: `{"ticket_id", "reason"}` tanpa isi — muat ulang lewat API (hak lihat tetap dicek). |
| `ticket.message` / `ticket.message.updated` | `ticket:{id}` | Pesan diskusi baru atau berubah (dikirim sebagai feedback). Isi = resource pesan tiket. |
| `ticket.event` | `ticket:{id}` | Log tiket baru (status, prioritas, tenggat, peserta, feedback, kesimpulan). |
| `ticket.updated` | `ticket:{id}` | Data tiket berubah — muat ulang detailnya. |
| `contacts.changed` | `ws:{id}` | Kontak dibuat/diubah/digabung/dihapus/diimpor: `{"contact_id", "reason"}`. |
| `contacts.settings` | `ws:{id}` | Label Onix atau field kustom berubah. |
| `kb.changed` | `ws:{id}` | Kategori atau artikel knowledge base berubah, termasuk selesai/gagal diproses untuk pencarian makna: `{"article_id"}` atau `{"category_id"}`. |
| `pipeline.changed` | `ws:{id}` | Lead, workflow, atau pengaturan Pipeline berubah: `{"lead_id", "pipeline_id", "reason"}` tanpa isi — muat ulang lewat API (kepemilikan data tetap dicek). |
| `clients.changed` | `ws:{id}` | Klien dibuat/diubah/dihapus atau kontak ditautkan: `{"client_id"}`. |
| `broadcasts.changed` | `ws:{id}` | Broadcast dibuat/diubah/dimulai/dijeda/selesai, progres kirim, atau tanda terima/balasan penerima: `{"broadcast_id", "reason"}` — muat ulang lewat API. |
| `segments.changed` | `ws:{id}` | Segmen dibuat/diubah/dihapus: `{"segment_id"}`. |

Kanal `conv:{id}` hanya diberikan bila pengguna boleh melihat percakapan itu (akses channel + aturan tim). Saat percakapan di-assign ke tim/orang lain, yang kehilangan akses otomatis dicabut dari kanalnya.

Kanal `ticket:{id}` hanya diberikan bila pengguna boleh membuka tiket itu (peserta, anggota tim untuk tiket tanpa penanggung jawab, atau melihat semua data).

Widget livechat memakai kanal terpisah `visitor:{sesi}` (token dari `POST /livechat/{key}/realtime`) dengan event `message`, `queue`, `typing`, dan `read` — lihat [Livechat](https://onix.sassly.ai/id/docs/livechat).
