v1

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 untuk POST /devices/register. Ditanam di firmware (include/config.h), nilainya = DEVICE_PROVISION_KEY di .env server.
  • 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 (meta csrf-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)

POST /api/v1/devices/register Header X-Provision-Key

Daftar/daftar ulang robot berdasarkan MAC. Mengembalikan token device (sekali tampil) dan URL WebSocket.

Body (JSON)Keterangan
macstring · wajib · AA:BB:CC:DD:EE:FF
firmwarestring · 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
        }
    }
}
GET /api/v1/device/me Bearer token device

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
        }
    }
}
POST /api/v1/device/heartbeat Bearer token device

Tanda hidup (opsional; WebSocket sudah mengirim ping).

Body (JSON)Keterangan
firmwarestring · opsional
{
    "ok": true,
    "data": {
        "server_time": "2026-10-20T19:00:00+07:00"
    }
}
GET /api/v1/device/orders Bearer token device

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
        }
    ]
}
GET /api/v1/device/payment Bearer token device

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

GET /api/v1/health Tanpa auth

Status database & relay.

{
    "ok": true,
    "database": "up",
    "relay": "up"
}
GET /api/v1/info Tanpa auth

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"
    }
}
GET /api/v1/menu?q=teh leci Tanpa auth

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
            }
        ]
    }
}
GET /api/v1/menu Tanpa auth

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
                    }
                ]
            }
        ]
    }
}
GET /api/v1/payments/{code} Kode tagihan

Status tagihan beserta isi pesanannya (dipakai halaman bayar).

{
    "ok": true,
    "data": {
        "code": "PAY261020A1B2C3D4",
        "amount": 71000,
        "status": "paid",
        "orders": [
            "…"
        ]
    }
}
POST /api/v1/payments/{code}/simulate Kode tagihan

Mode simulasi saja: menandai tagihan lunas dan memberi tahu robot.

{
    "ok": true,
    "data": {
        "status": "paid"
    }
}
POST /webhooks/sassly-pay Signature HMAC

Forward URL Sassly Pay. Diverifikasi dengan X-Sassly-Signature (HMAC-SHA256 dari "{timestamp}.{body}"), idempoten per event_id.

{
    "ok": true
}

CMS (admin)

GET /api/v1/admin/stats Sesi admin + X-CSRF-Token

Statistik dashboard.

{
    "ok": true,
    "data": {
        "orders_today": 18,
        "revenue_today": 742000,
        "devices_online": 3
    }
}
GET /api/v1/admin/orders?date=YYYY-MM-DD Sesi admin + X-CSRF-Token

Pesanan untuk papan dapur.

{
    "ok": true,
    "data": {
        "orders": [
            "…"
        ],
        "max_id": 120
    }
}
POST /api/v1/admin/orders/{id}/status Sesi admin + X-CSRF-Token

Ubah status dapur: new, preparing, served, completed, cancelled.

Body (JSON)Keterangan
statusstring · wajib
{
    "ok": true,
    "data": {
        "status": "served"
    }
}
POST /api/v1/admin/menu/{id}/availability Sesi admin + X-CSRF-Token

Tandai menu tersedia/habis.

Body (JSON)Keterangan
availableboolean
{
    "ok": true,
    "data": {
        "id": 7,
        "available": false
    }
}
POST /api/v1/admin/devices/{id}/command Sesi admin + X-CSRF-Token

Kirim perintah ke robot: say (robot mengucapkan teks saat percakapan aktif), end_session, reload.

Body (JSON)Keterangan
typesay | end_session | reload
textstring · untuk say
{
    "ok": true,
    "data": {
        "queued": true
    }
}
GET /api/v1/admin/conversations/{id}/messages?after=ID Sesi admin + X-CSRF-Token

Transkrip live.

{
    "ok": true,
    "data": {
        "status": "active",
        "messages": [
            {
                "id": 81,
                "role": "user",
                "content": "Aku mau paket ayam kurasan dua"
            }
        ]
    }
}

WebSocket robot

WS/ws/device Bearer token device

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

→hello

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
}
→start

Layar di-tap: mulai percakapan. Server membuka sesi AI lalu robot menyapa.

{
    "type": "start"
}
→listening

Robot selesai memutar jawaban dan membuka mic lagi (memulai timer diam).

{
    "type": "listening"
}
→stop

Akhiri percakapan (tap “Selesai”, atau setelah turn.done end=true diputar habis).

{
    "type": "stop",
    "reason": "done"
}
→ping

Opsional, dibalas pong.

{
    "type": "ping",
    "t": 1234
}
→(binary)

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

←hello.ok

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
    }
}
←session.started / session.ended

Percakapan dimulai / selesai (kembali ke standby).

{
    "type": "session.ended",
    "reason": "done"
}
←state

hearing = pelanggan bicara, thinking = berhenti bicara (tutup mic), menunggu jawaban.

{
    "type": "state",
    "value": "thinking"
}
←user.text

Transkrip ucapan pelanggan.

{
    "type": "user.text",
    "text": "Aku mau paket ayam kurasan dua ya"
}
←audio.start / audio.end

Awal/akhir stream suara robot untuk satu jawaban (id = item).

{
    "type": "audio.start",
    "id": "item_C9…"
}
←(binary)

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.

←bot.delta / bot.done

Caption yang diucapkan robot (tampilkan selaras pemutaran).

{
    "type": "bot.delta",
    "id": "item_C9…",
    "text": "Siap kak, dua paket"
}
←turn.done

Jawaban lengkap. Setelah audio habis diputar: buka mic & kirim listening — atau akhiri jika end=true.

{
    "type": "turn.done",
    "end": false
}
←cart

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
    }
}
←order.placed

Pesanan dikirim ke dapur — tampilkan kartu sukses + nomor antrean.

{
    "type": "order.placed",
    "order": {
        "code": "KRS-261020-7F3A",
        "queue_no": 12,
        "total": 66000,
        "items": [
            "…"
        ]
    }
}
←payment

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"
    }
}
←payment.status

Tagihan lunas → tutup QR, tampilkan sukses.

{
    "type": "payment.status",
    "code": "PAY261020A1B2C3D4",
    "status": "paid",
    "amount": 71000
}
←order.status / toast / error

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)

ToolFungsi
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/menu dan /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

HTTPArti
401Token device / provision key / sesi admin tidak valid
404Data tidak ditemukan
409Konflik status (mis. tagihan sudah tidak berlaku)
419CSRF token CMS kedaluwarsa
422Validasi gagal — lihat error.fields
500Kesalahan server (dicatat di storage/logs)