Dokumentasi API Marki Robot
Robot ESP32 di meja berbicara dengan pelanggan lewat OpenAI Realtime (speech-to-speech). Robot tidak pernah memegang API key OpenAI: audio dialirkan ke server ini, server meneruskan ke OpenAI, menjalankan tool (keranjang, pesanan, pembayaran) terhadap database CMS, lalu mengirim suara + caption + event UI balik ke robot.
# Base URL REST https://robot.sassly.ai/api/v1 WebSocket wss://robot.sassly.ai/ws/device # Format respons REST { "ok": true, "data": … } { "ok": false, "error": { "code": 422, "message": "Data tidak valid", "fields": { … } } }
Autentikasi
- Provision key — header
X-Provision-Key, hanya untukPOST /devices/register. Ditanam di firmware (include/config.h), nilainya =DEVICE_PROVISION_KEYdi.envserver. - Token device —
Authorization: Bearer dev_…untuk REST robot dan WebSocket. Disimpan robot di NVS; bisa diganti dari CMS (Robot & Meja). - Admin — cookie sesi dari login Sassly (auth.sassly.ai, OIDC + PKCE) dan header
X-CSRF-Token(metacsrf-token) untuk endpoint/admin.
Alur robot
1. Boot → WiFi (portal setup bila belum ada) → POST /devices/register (sekali, simpan token)
2. Buka WebSocket wss://robot.sassly.ai/ws/device (Authorization: Bearer token) → kirim hello
3. Standby: wajah tidur + "Tap saya untuk berbicara"
4. Tap → {"type":"start"} → robot menyapa: "Halo kak! Ada yang bisa dibantu? Mau pesan apa?"
5. Giliran bicara (half-duplex, tanpa gema):
robot putar suara + caption → turn.done → buka mic → {"type":"listening"}
pelanggan bicara → state hearing → state thinking (mic ditutup) → jawaban berikutnya
6. AI memanggil tool → event cart / order.placed / payment ke layar
7. Pamit / diam 25 dtk → salam penutup → turn.done end=true → {"type":"stop"} → standby
8. Tap lagi kapan saja untuk menambah pesanan atau minta bill (QR)
Robot (device)
Daftar/daftar ulang robot berdasarkan MAC. Mengembalikan token device (sekali tampil) dan URL WebSocket.
| Body (JSON) | Keterangan |
|---|---|
| mac | string · wajib · AA:BB:CC:DD:EE:FF |
| firmware | string · opsional |
{
"ok": true,
"data": {
"token": "dev_9Qm…",
"device": {
"id": 1,
"name": "Robot Meja 1",
"table": "Meja 1",
"status": "online"
},
"ws_url": "wss://robot.sassly.ai/ws/device",
"config": {
"restaurant": "Ayam Goreng Kurasan",
"robot_name": "Marki",
"standby_text": "Tap saya untuk berbicara",
"sample_rate": 24000
}
}
}
Profil robot, nomor meja, dan konfigurasi tampilan.
{
"ok": true,
"data": {
"device": {
"id": 1,
"table": "Meja 1"
},
"ws_url": "wss://robot.sassly.ai/ws/device",
"config": {
"idle_timeout_s": 25
}
}
}
Tanda hidup (opsional; WebSocket sudah mengirim ping).
| Body (JSON) | Keterangan |
|---|---|
| firmware | string · opsional |
{
"ok": true,
"data": {
"server_time": "2026-10-20T19:00:00+07:00"
}
}
Pesanan meja ini yang belum dibayar (12 jam terakhir).
{
"ok": true,
"data": [
{
"code": "KRS-261020-7F3A",
"queue_no": 12,
"status": "preparing",
"payment_status": "unpaid",
"items": [
{
"name": "Paket Nasi + Ayam Kurasan",
"qty": 2,
"price": 33000,
"total": 66000
}
],
"total": 66000
}
]
}
Tagihan yang masih menunggu dibayar (untuk menampilkan ulang QR setelah restart).
{
"ok": true,
"data": {
"code": "PAY261020A1B2C3D4",
"amount": 71000,
"status": "pending",
"qr": "https://robot.sassly.ai/pay/PAY261020A1B2C3D4",
"expires_at": "2026-10-20 19:15:00"
}
}
Publik
Status database & relay.
{
"ok": true,
"database": "up",
"relay": "up"
}
Info restoran (alamat, jam buka, kontak, fasilitas, FAQ) — satu-satunya sumber info non-menu yang diucapkan robot.
{
"ok": true,
"data": {
"name": "Ayam Goreng Kurasan",
"robot_name": "Marki",
"address": "Jl. …",
"opening_hours": "10.00–22.00",
"phone": "",
"facilities": "WiFi, musala",
"faq": "",
"payment_methods": "Scan QR di layar robot, atau bayar di kasir"
}
}
Cari menu (nama, sebutan lain, kategori, keterangan; toleran salah ketik satu huruf). Dipakai juga oleh tool AI search_menu.
{
"ok": true,
"data": {
"query": "teh leci",
"items": [
{
"id": 54,
"category": "Minuman",
"name": "Lychee Teh",
"price": 15000,
"available": true
}
]
}
}
Menu yang tersedia per kategori.
{
"ok": true,
"data": {
"restaurant": "Ayam Goreng Kurasan",
"categories": [
{
"id": 1,
"name": "Paket",
"items": [
{
"id": 4,
"name": "Paket Nasi + Ayam Kurasan",
"price": 33000,
"available": true,
"recommended": true,
"spicy_level": 2
}
]
}
]
}
}
Status tagihan beserta isi pesanannya (dipakai halaman bayar).
{
"ok": true,
"data": {
"code": "PAY261020A1B2C3D4",
"amount": 71000,
"status": "paid",
"orders": [
"…"
]
}
}
Mode simulasi saja: menandai tagihan lunas dan memberi tahu robot.
{
"ok": true,
"data": {
"status": "paid"
}
}
Forward URL Sassly Pay. Diverifikasi dengan X-Sassly-Signature (HMAC-SHA256 dari "{timestamp}.{body}"), idempoten per event_id.
{
"ok": true
}
CMS (admin)
Statistik dashboard.
{
"ok": true,
"data": {
"orders_today": 18,
"revenue_today": 742000,
"devices_online": 3
}
}
Pesanan untuk papan dapur.
{
"ok": true,
"data": {
"orders": [
"…"
],
"max_id": 120
}
}
Ubah status dapur: new, preparing, served, completed, cancelled.
| Body (JSON) | Keterangan |
|---|---|
| status | string · wajib |
{
"ok": true,
"data": {
"status": "served"
}
}
Tandai menu tersedia/habis.
| Body (JSON) | Keterangan |
|---|---|
| available | boolean |
{
"ok": true,
"data": {
"id": 7,
"available": false
}
}
Kirim perintah ke robot: say (robot mengucapkan teks saat percakapan aktif), end_session, reload.
| Body (JSON) | Keterangan |
|---|---|
| type | say | end_session | reload |
| text | string · untuk say |
{
"ok": true,
"data": {
"queued": true
}
}
Transkrip live.
{
"ok": true,
"data": {
"status": "active",
"messages": [
{
"id": 81,
"role": "user",
"content": "Aku mau paket ayam kurasan dua"
}
]
}
}
WebSocket robot
Satu koneksi per robot, selalu tersambung (ping tiap 20 detik). Pesan teks = JSON kontrol, pesan biner = audio mono 24 kHz: PCM16 LE, atau IMA ADPCM bila robot meminta codec: "adpcm" di hello dan server menyetujuinya di hello.ok (¼ ukuran — suara tetap lancar di WiFi lambat; bisa dimatikan di Pengaturan). Blok ADPCM = int16 LE prediktor + uint8 indeks step + uint8 flag (bit0: nibble terakhir padding) + nibble 4-bit (nibble rendah dulu); tiap blok bisa didekode sendiri. Browser (Simulator CMS) boleh mengirim token lewat ?token=.
GET /ws/device HTTP/1.1 Host: robot.sassly.ai Upgrade: websocket Connection: Upgrade Authorization: Bearer dev_9Qm… Sec-WebSocket-Version: 13
Robot → server
Setelah tersambung. Server membalas hello.ok. codec=adpcm (opsional) meminta audio IMA ADPCM; block = sampel per blok mic.
{
"type": "hello",
"fw": "1.1.0",
"rate": 24000,
"codec": "adpcm",
"block": 960
} Layar di-tap: mulai percakapan. Server membuka sesi AI lalu robot menyapa.
{
"type": "start"
} Robot selesai memutar jawaban dan membuka mic lagi (memulai timer diam).
{
"type": "listening"
} Akhiri percakapan (tap “Selesai”, atau setelah turn.done end=true diputar habis).
{
"type": "stop",
"reason": "done"
} Opsional, dibalas pong.
{
"type": "ping",
"t": 1234
} Audio mic, 24 000 Hz mono, ±40 ms per blok. Codec pcm: PCM16 little-endian. Codec adpcm: satu atau lebih blok ADPCM berurutan (boleh digabung saat koneksi lambat). Hanya saat state listening/hearing.
Server → robot
Profil, konfigurasi, isi keranjang, dan codec audio yang dipakai (pcm | adpcm) untuk semua pesan biner berikutnya.
{
"type": "hello.ok",
"codec": "adpcm",
"device": {
"id": 1,
"table": "Meja 1"
},
"config": {
"restaurant": "Ayam Goreng Kurasan",
"robot_name": "Marki",
"standby_text": "Tap saya untuk berbicara",
"sample_rate": 24000,
"idle_timeout_s": 25
},
"cart": {
"items": [],
"count": 0,
"total": 0
}
} Percakapan dimulai / selesai (kembali ke standby).
{
"type": "session.ended",
"reason": "done"
} hearing = pelanggan bicara, thinking = berhenti bicara (tutup mic), menunggu jawaban.
{
"type": "state",
"value": "thinking"
} Transkrip ucapan pelanggan.
{
"type": "user.text",
"text": "Aku mau paket ayam kurasan dua ya"
} Awal/akhir stream suara robot untuk satu jawaban (id = item).
{
"type": "audio.start",
"id": "item_C9…"
} Suara robot, 24 kHz mono, ±128 ms per pesan: PCM16 LE (codec pcm) atau satu blok ADPCM (codec adpcm). Putar berurutan; mulut digerakkan dari amplitudo.
Caption yang diucapkan robot (tampilkan selaras pemutaran).
{
"type": "bot.delta",
"id": "item_C9…",
"text": "Siap kak, dua paket"
} Jawaban lengkap. Setelah audio habis diputar: buka mic & kirim listening — atau akhiri jika end=true.
{
"type": "turn.done",
"end": false
} Isi keranjang berubah (tool add_to_cart, update_cart_item, …).
{
"type": "cart",
"cart": {
"items": [
{
"name": "Paket Nasi + Ayam Kurasan",
"qty": 2,
"total": 66000
}
],
"count": 2,
"total": 66000
}
} Pesanan dikirim ke dapur — tampilkan kartu sukses + nomor antrean.
{
"type": "order.placed",
"order": {
"code": "KRS-261020-7F3A",
"queue_no": 12,
"total": 66000,
"items": [
"…"
]
}
} Tampilkan QR (field qr) dan nominal.
{
"type": "payment",
"payment": {
"code": "PAY261020A1B2C3D4",
"amount": 71000,
"status": "pending",
"qr": "https://robot.sassly.ai/pay/PAY261020A1B2C3D4",
"expires_at": "2026-10-20 19:15:00"
}
} Tagihan lunas → tutup QR, tampilkan sukses.
{
"type": "payment.status",
"code": "PAY261020A1B2C3D4",
"status": "paid",
"amount": 71000
} Info tambahan dari dapur/CMS, atau error sesi AI.
{
"type": "error",
"code": "ai_unavailable",
"message": "Maaf, AI sedang tidak bisa dihubungi."
} Tool AI (dijalankan server)
| Tool | Fungsi |
|---|---|
| search_menu(query) | Cari menu — data & aturan sama dengan GET /api/v1/menu?q= |
| get_restaurant_info() | Info restoran — sama dengan GET /api/v1/info |
| add_to_cart(item_id, qty, note?) | Tambah menu ke keranjang meja → event cart |
| update_cart_item(item_id, qty) | Ubah jumlah (0 = hapus) → event cart |
| clear_cart() | Kosongkan keranjang |
| view_cart() | Baca isi & total keranjang |
| place_order(note?) | Kirim ke dapur setelah pelanggan konfirmasi → event order.placed, muncul di papan dapur CMS |
| get_order_status() | Status pesanan meja ini |
| request_payment() | Tagihan semua pesanan belum lunas → event payment (QR) |
| end_conversation(reason?) | Pamit lalu kembali ke standby |
Robot hanya bicara dari data
- Instruksi sesi berisi DAFTAR MENU (id, nama, harga, status, keterangan) dan INFO RESTORAN yang dibangun dari database yang sama dengan
/api/v1/menudan/api/v1/info, diperbarui di awal setiap percakapan. - Robot dilarang mengarang menu, harga, promo, stok, waktu, atau info restoran; yang tidak ada di data dijawab “belum ada infonya”.
- Topik di luar restoran ditolak sopan lalu kembali ke menu. Ucapan tidak jelas → robot minta mengulang.
- Isi & total keranjang, nomor antrean, dan tagihan selalu dari hasil tool (database), bukan hitungan AI.
Toleransi suara
Relay menghitung level tiap frame mic (dBFS). Frame di bawah ambang suara (CMS → Pengaturan → Toleransi suara) diganti hening sebelum dikirim ke AI, dengan waktu tahan agar akhir kalimat tidak terpotong. Level live tiap robot: GET /api/v1/admin/mic-levels.
Kode error
| HTTP | Arti |
|---|---|
| 401 | Token device / provision key / sesi admin tidak valid |
| 404 | Data tidak ditemukan |
| 409 | Konflik status (mis. tagihan sudah tidak berlaku) |
| 419 | CSRF token CMS kedaluwarsa |
| 422 | Validasi gagal — lihat error.fields |
| 500 | Kesalahan server (dicatat di storage/logs) |