# Channel API

Sambungkan aplikasi, sistem mitra, atau bot Anda ke Onix secara dua arah: sistem Anda mengirim pesan pelanggan ke Onix lewat satu endpoint, dan balasan agen dikirim ke webhook Anda. Percakapannya tampil di Interaction seperti channel lain, real-time, dengan nama & ikon channel sendiri.

> Channel API berbeda dengan **API key workspace** (Pengaturan → API, lihat [API](https://onix.sassly.ai/id/docs/api)). API key untuk membaca dan mengelola data Onix; **kunci channel** (`onx_ch_…`) hanya bisa mengirim pesan masuk ke satu channel API itu.

## Membuat channel API

1. Buka **Kelola → Channel**, klik **Tambah channel**, lalu pilih **Channel API** (paket Pro/Custom; butuh akses mengelola channel).
2. Isi **nama channel** (mis. nama aplikasi Anda — tampil di Interaction & filter) dan **URL webhook** (boleh diisi nanti), lalu klik **Buat channel API**.
3. Salin **kunci channel** (`onx_ch_…`) dan **secret webhook** (`whsec_…`). Kunci channel hanya tampil sekali — simpan di server Anda; bila hilang, ganti kunci. Secret bisa dilihat lagi di pengaturan channel.
4. Opsional: unggah **ikon** (PNG/JPG/WebP persegi, maks 512 KB) supaya percakapan dari sistem ini mudah dikenali di Interaction.

Satu channel API memakai satu slot channel; kuotanya digabung dengan nomor WhatsApp, alamat email, dan livechat (Pro 4 channel). Bila paket turun ke Free, channel API dijeda dan pesan masuk ditolak (402) sampai paket aktif lagi.

## Mengirim pesan pelanggan ke Onix

Kirim setiap pesan pelanggan dari sistem Anda ke endpoint ini dengan kunci channel di header `Authorization`:

```
curl -s -X POST "https://onix.sassly.ai/api/v1/channel-api/messages" \
  -H "Authorization: Bearer $ONIX_CHANNEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "conversation_id": "order-1029",
    "contact": {"id": "u_981", "name": "Budi Santoso", "email": "budi@contoh.com", "phone": "6281234567890"},
    "message": {"id": "m_5521", "text": "Halo, pesanan saya belum sampai",
                "attachments": [{"url": "https://cdn.tokoanda.com/foto.jpg", "name": "foto.jpg"}]}
  }'
```

| Kolom | Keterangan |
| --- | --- |
| `conversation_id` | Opsional, maks 100 karakter. ID percakapan di sistem Anda (mis. nomor pesanan). Bawaan = `contact.id` (satu percakapan per pelanggan). Pesan dengan `conversation_id` yang sama masuk ke percakapan yang sama selama masih terbuka. |
| `contact.id` | **Wajib**, maks 100 karakter. ID pelanggan di sistem Anda. |
| `contact.name`, `contact.email`, `contact.phone` | Opsional. Pelanggan dicocokkan lewat `contact.id`, lalu email atau nomor HP — pelanggan yang sudah pernah chat lewat WhatsApp, email, atau livechat masuk ke kontak yang sama. |
| `message.id` | **Wajib**, unik per channel, maks 100 karakter. Mengirim ulang pesan dengan `message.id` yang sama aman: dijawab `200` dengan `"duplicate": true` dan tidak dicatat dua kali. |
| `message.text` | Maks 4.000 karakter. Wajib bila tanpa lampiran. |
| `message.attachments` | Opsional, maks 5: `[{"url", "name"}]`. URL harus https ke host publik; Onix mengunduh dan menyimpannya privat (maks 10 MB per file, pengalihan/redirect tidak diikuti). Lampiran ke-2 dan seterusnya menjadi pesan sendiri dengan id `{message.id}#2`, `#3`, … |
| `message.sent_at` | Opsional, ISO 8601 (mis. `2026-10-11T03:00:00Z`). Bawaan = waktu diterima; waktu di masa depan dipotong ke sekarang. |

Hasil `201 Created` (atau `200` untuk duplikat):

```
{"data": {"message_id": 678, "interaction_id": 45, "interaction_number": "INT-000045", "duplicate": false}}
```

Punya file di server Anda sendiri? Kirim sebagai `multipart/form-data` dengan `files[]`; kolom lain memakai tanda kurung:

```
curl -s -X POST "https://onix.sassly.ai/api/v1/channel-api/messages" \
  -H "Authorization: Bearer $ONIX_CHANNEL_KEY" \
  -F "contact[id]=u_981" -F "message[id]=m_5522" -F "message[text]=Ini struknya" \
  -F "files[]=@struk.pdf"
```

- Percakapan mengikuti aturan yang sama dengan channel lain: pesan ke percakapan yang sudah Solved/Closed membuka percakapan baru (nomor INT baru).
- Salam awal & pesan di luar jam kerja (bila dinyalakan di pengaturan channel) dikirim ke sistem Anda lewat webhook dengan `sender.type` = `"auto"`.
- Pesan masuk muncul di Interaction secara real-time, dengan ikon dan nama channel API Anda.

## Menerima balasan agen (webhook)

Setiap balasan agen dan balasan otomatis dikirim Onix ke URL webhook sebagai `POST` JSON (event `message.created`, selalu dikirim). Event `conversation.updated` (status Solved/Closed/dibuka lagi atau penanggung jawab berubah) bisa dinyalakan di pengaturan channel. URL webhook harus **https** ke host publik (bukan alamat jaringan internal).

```
POST https://api.tokoanda.com/onix/webhook
Content-Type: application/json
User-Agent: Onix-Webhook/1.0 (+https://onix.sassly.ai/docs/api-channel)
X-Onix-Event: message.created
X-Onix-Delivery: 0c6f7a52-2b1e-4b8e-9d0a-5d7f3c1e9a41
X-Onix-Timestamp: 1791630130
X-Onix-Signature: sha256=8f2c…

{
  "event": "message.created",
  "channel": {"id": 9, "name": "Aplikasi Mitra"},
  "conversation": {"id": "order-1029", "interaction_id": 45, "number": "INT-000045", "status": "open"},
  "contact": {"id": "u_981", "name": "Budi Santoso"},
  "message": {
    "id": 680,
    "type": "document",
    "text": "Halo kak, ini resinya ya",
    "attachments": [{"name": "resi.pdf", "mime": "application/pdf", "size": 48211, "url": "https://onix.sassly.ai/api/v1/channel-api/files/…"}],
    "sender": {"type": "agent", "name": "Sari"},
    "sent_at": "2026-10-11T03:02:10Z"
  }
}
```

```
{
  "event": "conversation.updated",
  "change": "status",
  "channel": {"id": 9, "name": "Aplikasi Mitra"},
  "conversation": {"id": "order-1029", "interaction_id": 45, "number": "INT-000045", "status": "solved", "assignee": {"name": "Sari"}},
  "contact": {"id": "u_981", "name": "Budi Santoso"},
  "updated_at": "2026-10-11T03:20:00Z"
}
```

- `conversation.id` dan `contact.id` adalah ID dari sistem Anda, jadi Anda bisa meneruskan balasan tanpa menyimpan ID Onix. `message.id` adalah ID pesan Onix (untuk status terkirim/dibaca).
- `sender.type`: `agent` (balasan agen) atau `auto` (balasan otomatis). Satu pesan Onix membawa paling banyak satu lampiran; `url` lampiran bertanda tangan dan berlaku 7 hari tanpa login.
- Endpoint Anda harus menjawab **2xx dalam 10 detik**. Bila tidak, pengiriman dicoba ulang dengan jeda **1 menit, 5 menit, 15 menit, 1 jam, 6 jam** — jumlah coba ulang diatur per channel (0–5, bawaan 5). Setelah itu pesan ditandai **Gagal** di Interaction beserta alasannya, dan agen bisa **Kirim ulang**.
- Pesan dikirim **berurutan per percakapan**: pesan berikutnya menunggu sampai yang sebelumnya terkirim atau gagal.
- `X-Onix-Delivery` sama di setiap coba ulang — simpan dan abaikan yang sudah pernah diproses.
- URL webhook belum diisi? Balasan agen tetap tersimpan di Onix tetapi berstatus Gagal "URL webhook belum diatur".

## Memeriksa tanda tangan webhook

`X-Onix-Signature` = `sha256=` + HMAC-SHA256 (heksadesimal) dari `X-Onix-Timestamp + "." + body mentah` dengan secret webhook (`whsec_…`) sebagai kunci. Hitung ulang dari body **sebelum** di-parse sebagai JSON, bandingkan dengan fungsi waktu-konstan, dan tolak timestamp yang lebih dari 5 menit dari jam server Anda (mencegah pengiriman ulang oleh pihak lain).

### PHP

```
<?php
$secret    = getenv('ONIX_WEBHOOK_SECRET');          // whsec_…
$body      = file_get_contents('php://input');        // body mentah
$timestamp = $_SERVER['HTTP_X_ONIX_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_ONIX_SIGNATURE'] ?? '';

$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);
if (!hash_equals($expected, $signature) || abs(time() - (int) $timestamp) > 300) {
    http_response_code(401);
    exit;
}
$event = json_decode($body, true);
if ($event['event'] === 'message.created') {
    // Teruskan $event['message']['text'] ke pelanggan $event['contact']['id']
    // di percakapan $event['conversation']['id'].
}
http_response_code(200);
```

### Node.js (Express)

```
import crypto from 'node:crypto';
import express from 'express';

const app = express();
// Body mentah: tanda tangan dihitung dari byte yang dikirim Onix.
app.post('/onix/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const timestamp = req.get('X-Onix-Timestamp') || '';
  const signature = req.get('X-Onix-Signature') || '';
  const expected = 'sha256=' + crypto.createHmac('sha256', process.env.ONIX_WEBHOOK_SECRET)
    .update(timestamp + '.').update(req.body).digest('hex');
  const valid = signature.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
    && Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300;
  if (!valid) return res.sendStatus(401);
  const event = JSON.parse(req.body);
  // … teruskan event.message ke pelanggan event.contact.id
  res.sendStatus(200);
});
```

### Python (Flask)

```
import hashlib, hmac, json, os, time
from flask import Flask, abort, request

app = Flask(__name__)

@app.post("/onix/webhook")
def onix_webhook():
    body = request.get_data()  # byte mentah
    timestamp = request.headers.get("X-Onix-Timestamp", "")
    signature = request.headers.get("X-Onix-Signature", "")
    expected = "sha256=" + hmac.new(os.environ["ONIX_WEBHOOK_SECRET"].encode(),
                                    timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, signature) or abs(time.time() - int(timestamp or 0)) > 300:
        abort(401)
    event = json.loads(body)
    # … teruskan event["message"] ke pelanggan event["contact"]["id"]
    return "", 200
```

## Status terkirim & dibaca (opsional)

Supaya agen melihat balasannya sudah sampai, kirim `POST https://onix.sassly.ai/api/v1/channel-api/messages/{id}/status` dengan kunci channel dan `{"status": "delivered"}` atau `{"status": "read"}`, memakai `message.id` dari webhook. Status hanya naik (Terkirim → Diterima → Dibaca) dan tampil langsung di Interaction.

## Pengaturan & alat uji

| Tab | Isi |
| --- | --- |
| Webhook | Nama channel, ikon, URL webhook, event yang dikirim, jumlah coba ulang, serta **Tes webhook** (mengirim event `ping` sekarang dan menampilkan kode HTTP & lamanya) dan **Kirim pesan uji** (mensimulasikan pesan masuk dari sistem Anda; balas pesan itu di Interaction untuk mencoba webhook). |
| Kredensial | Endpoint pesan masuk, kunci channel (awalan & kapan terakhir dipakai) dengan tombol **Ganti kunci** (kunci lama langsung ditolak), serta secret webhook: **Lihat secret** & **Ganti secret**. |
| Jam kerja & otomatis | Ikuti jam kerja workspace atau jam kerja sendiri, salam awal, dan pesan di luar jam kerja — sama dengan semua channel (lihat [Livechat](https://onix.sassly.ai/id/docs/livechat) bagian jam kerja). |
| Riwayat | 50 pengiriman webhook terakhir: waktu, event, status (Terkirim, Dicoba ulang, Gagal), kode HTTP, lama, dan keterangan. |

## Di Interaction

- Percakapan channel API masuk tab **Chat** dengan ikon yang Anda unggah (atau ikon API bawaan) dan nama channel di item & filter Channel.
- Agen membalas dengan teks, satu lampiran, template, artikel KB, atau catatan internal (catatan tidak pernah dikirim ke sistem Anda). Reaksi, edit, tarik pesan, dan kutip tidak tersedia.
- Status balasan: **Memproses** (menunggu webhook) → **Terkirim** (endpoint menjawab 2xx) → **Diterima**/**Dibaca** bila sistem Anda mengirim status. **Gagal** menampilkan alasannya (mis. "Endpoint menjawab HTTP 500") dengan tombol Kirim ulang.
- Penugasan mengikuti aturan biasa (tanpa antrian Ambil chat — itu khusus livechat). Target respons channel API punya nilai sendiri (bawaan 15 menit kerja) — lihat [KPI & SLA](https://onix.sassly.ai/id/docs/kpi).

## Kode galat

Galat memakai format yang sama dengan API Onix: `{"error": {"code", "message", "fields"}}`.

| HTTP | Artinya |
| --- | --- |
| `401` | Kunci channel tidak ada, salah, atau sudah diganti. |
| `402` | Paket workspace bukan Pro/Custom — channel API dijeda. |
| `404` | Status: pesan tidak ditemukan di channel ini (bukan balasan agen channel itu). |
| `409` | Channel API sudah dihapus atau sedang tidak aktif. |
| `422` | Isian tidak valid (per kolom di `fields`, mis. `message.id`), lampiran tidak bisa diunduh, atau lebih besar dari 10 MB. |
| `429` | Lebih dari 120 request per menit per kunci channel. Tunggu sesuai header `Retry-After`. |

## Keamanan

- Kunci channel hanya disimpan sebagai sidik (hash) dan bisa diganti kapan saja; secret webhook disimpan terenkripsi. Simpan keduanya di server Anda, jangan di aplikasi mobile atau JavaScript browser.
- Onix hanya menghubungi URL https ke host publik (URL webhook & URL lampiran) dan tidak mengikuti pengalihan, supaya isian tidak bisa dipakai menjangkau jaringan internal.
- Selalu periksa tanda tangan dan timestamp webhook sebelum memprosesnya.
- Menghapus channel API langsung menghentikan kuncinya dan membatalkan webhook yang masih menunggu coba ulang; riwayat percakapan diarsipkan.

Spesifikasi lengkap (termasuk bagian `webhooks` OpenAPI 3.1): [`/docs/openapi.yaml`](https://onix.sassly.ai/docs/openapi.yaml).
