Developer Docs

REST API & Webhook untuk integrasi AI Doodle RNV4

API ini memungkinkan aplikasi eksternal — seperti RNV4 Studio atau aplikasi buatan Anda sendiri — untuk:

  • Mengirim foto ke server dan meminta AI men-generate gambar doodle
  • Memantau status generate secara polling atau menerima notifikasi via Webhook
  • Mengambil hasil gambar yang sudah selesai
  • Mengecek saldo kredit akun

Base URL: https://app.ruskomponen.uk

Semua response dalam format application/json kecuali endpoint download gambar.

Butuh spesifikasi fisik robotnya (dimensi, pin, sensor)? Lihat Dok. Hardware RNV4 →

Quick Start

  1. 1Daftar akun di https://app.ruskomponen.uk/register
  2. 2Buat API Key di Dashboard → Developer → API Keys
  3. 3Kirim foto ke POST /api/v1/ai/generate dengan header X-API-Token: <token>
  4. 4Poll GET /api/v1/ai/status/{gen_id} hingga status == "done", lalu ambil gambar
# Contoh cURL — generate gambar curl -X POST https://app.ruskomponen.uk/api/v1/ai/generate \ -H "X-API-Token: rnv4-your-token-here" \ -F "[email protected]" \ -F "prompt=Ubah foto ini menjadi line art hitam putih" \ -F "robot_id=RNV4-001"

Autentikasi

Semua endpoint API memerlukan API Key yang dikirim via HTTP header:

X-API-Token: rnv4-your-token-here

Cara mendapatkan API Key

Login ke Dashboard → tab Developer → bagian API Keys → klik Buat Token Baru. Key hanya ditampilkan sekali saat dibuat — simpan di tempat aman.

Polling vs Webhook

Setelah memanggil POST /api/v1/ai/generate, server memproses gambar di background. Ada dua cara untuk mengetahui hasilnya — pilih sesuai jenis aplikasi Anda:

🔄

Polling

Client bertanya ke server secara berkala hingga selesai.

Cocok untuk:

  • Desktop app (tidak punya public URL)
  • Script / CLI / quick testing
  • Di balik NAT / firewall
# Poll tiap 3 detik while True: r = requests.get(f"{BASE}/status/{gen_id}", headers=headers) if r.json()["status"] in ("done","failed"): break time.sleep(3)

Webhook

Server mengirim notifikasi ke URL Anda begitu selesai.

Cocok untuk:

  • Web app / backend dengan public URL
  • Real-time tanpa buang bandwidth
  • Integrasi ke sistem lain
# Server POST ke URL Anda saat selesai # { "event": "ai.generate.done", # "data": { "gen_id": 42, # "result_url": "..." } }
Bisa gunakan keduanya. Tidak ada larangan menggunakan polling dan mendaftarkan webhook sekaligus — misalnya polling sebagai fallback jika webhook gagal terkirim.

REST API

POST /api/v1/ai/generate

Kirim foto dan prompt. Server memotong kredit, lalu menjalankan Gemini di background. Hasilnya bisa diambil via polling atau Webhook.

Request (multipart/form-data)

FieldTipeKeterangan
photofile File gambar (JPEG/PNG, maks 10MB) wajib
promptstring Instruksi untuk AI. Jika kosong, digunakan prompt default (line art hitam putih).
robot_idstring ID robot pengirim (untuk log dan tracking).

Response

{ "gen_id": 42, "result_token": "a1b2c3d4e5f6...", "status": "pending", "queue_position": 0, "credit_cost": 1, "balance": 49 }
GET /api/v1/ai/status/{gen_id}

Cek status satu job generate. Poll endpoint ini hingga status bernilai done atau failed.

Nilai status

pendingAntri, belum diproses
processingSedang diproses oleh Gemini
doneSelesai — hasil siap diambil
failedGagal — kredit dikembalikan otomatis
expiredHasil sudah dihapus (TTL 1 jam setelah done)
# Response saat status = done { "gen_id": 42, "status": "done", "done_at": "2026-06-25T10:05:30Z", "result_token": "a1b2c3d4e5f6...", "expires_at": "2026-06-25T11:05:30Z" } # Response saat status = failed { "gen_id": 42, "status": "failed", "error_msg": "Gemini API timeout" }
GET /api/v1/ai/queue

Ambil status semua job generate milik akun sekaligus (50 terbaru). Lebih efisien dari poll /status satu-satu jika ada banyak job aktif.

{ "queue": [ { "gen_id": 44, "status": "processing", "created_at": "2026-06-25 10:06:00" }, { "gen_id": 42, "status": "done", "done_at": "2026-06-25 10:05:30", "result_token": "a1b2c3d4e5f6..." }, { "gen_id": 41, "status": "failed", "error": "Gemini API timeout" } ] }
GET /api/v1/ai/result/{gen_id}?token={result_token}

Download gambar hasil generate (PNG). Autentikasi via query param token (result_token dari response generate) atau header X-API-Token.

Hasil tersedia selama 1 jam setelah done. Setelah itu file dihapus dan status berubah ke expired.

Perlakukan result_token seperti kredensial

token di URL ini memberi akses unduh langsung ke hasil generate tanpa perlu X-API-Token — siapa pun yang memegang URL lengkap (termasuk result_url di payload webhook ai.generate.done) bisa mengunduhnya. Jangan tampilkan URL ini di halaman publik, log yang bisa diakses pihak lain, atau kirim ke analytics pihak ketiga. Berlaku hanya 1 jam, tapi tetap jaga kerahasiaannya selama itu.

curl "https://app.ruskomponen.uk/api/v1/ai/result/42?token=a1b2c3d4e5f6..." \ --output hasil.png
GET /api/v1/ai/history

Daftar generate history milik akun. Mendukung header X-API-Token (robot) maupun JWT (browser).

Query params

limit10 | 50 | 100Jumlah per halaman (default: 10)
pageintegerHalaman (default: 1)
periodstringFilter: all | today | week | month
{ "ok": true, "total": 42, "page": 1, "pages": 5, "limit": 10, "history": [ { "id": 123, "robot_id": "RNV4-ABC123", "prompt": "line art hitam putih", "status": "done", "model": "gemini-2.0-flash-exp-image-generation", "credits_used": 1, "sell_price_idr": 5000, "created_at": "2026-06-26T10:00:00", "done_at": "2026-06-26T10:00:08", "expires_at": "2026-06-26T11:00:08", "has_result": true, "result_token": "a1b2c3d4e5f6..." } ] }
GET /api/v1/ai/balance

Cek saldo kredit dan informasi rate limit.

{ "ok": true, "balance": 49, "rate_limit_secs": 10, "username": "johndoe" }
GET /api/v1/ai/rate-limit

Cek apakah akun boleh mengirim generate sekarang, dan berapa detik lagi jika belum boleh. Berguna untuk polling mode agar tidak kena error 429.

# Boleh generate sekarang { "can_generate": true, "wait_seconds": 0, "rate_limit_secs": 10 } # Masih harus tunggu { "can_generate": false, "wait_seconds": 6.3, "rate_limit_secs": 10 }
GET /api/v1/ai/token-info

Validasi API Key dan lihat informasi token yang sedang dipakai. Berguna saat pertama setup integrasi untuk memastikan key sudah benar sebelum mulai kirim request lain.

GET /api/v1/ai/token-info X-API-Token: rnv4_xxxxxxxxxxxx { "name": "Studio Utama", "token_last4": "a1b2", "created_at": "2026-01-15 08:00:00", "last_used_at": "2026-06-26 10:30:00", "username": "johndoe" }
GET /api/v1/ai/profile

Profil lengkap akun — saldo, jumlah robot, statistik generate, dan model aktif.

{ "username": "johndoe", "avatar_id": 5, "balance": 49, "robot_count": 2, "total_generations": 120, "generations_today": 5, "member_since": "2026-01-15", "model_key": "gemini-2.5-flash-image", "model_label": "Gemini 2.5 Flash Image", "model_credit_cost": 1 }

avatar_id0 berarti tampilkan inisial huruf pertama username (bukan emoji). Untuk avatar_id lain, lihat GET /api/v1/avatars utk daftar emoji+warnanya.

GET /api/v1/ai/models

Daftar model AI yang tersedia beserta harga kredit per generate untuk masing-masing model, dan current — model yang sedang aktif di akun Anda. Generate Gambar selalu memotong kredit sesuai credit_cost model yang sedang aktif saat itu — bukan parameter per-request, ganti model lewat POST /api/v1/ai/model di bawah kalau perlu.

{ "ok": true, "current": "gemini-2.5-flash-image", "models": [ { "model_key": "gemini-2.5-flash-image", "label": "2.5 Flash", "credit_cost": 1, "description": "Hemat kredit, resolusi hasil hingga 1024x1024." }, { "model_key": "gemini-3.1-flash-image", "label": "3.1 Flash", "credit_cost": 2, "description": "Kualitas & resolusi lebih tinggi (hingga 4K), kredit lebih mahal." } ] }
Harga & daftar model bisa berubah — dikelola admin, ditambah/dikurangi atau harganya disesuaikan kapan saja. Jangan hardcode credit_cost di aplikasi Anda; selalu ambil nilainya live dari endpoint ini sebelum menampilkan estimasi biaya ke pengguna.

Contoh dengan curl:

curl -H "X-API-Token: rnv4_xxxxxxxxxxxx" \ https://app.ruskomponen.uk/api/v1/ai/models
POST /api/v1/ai/model

Ganti model aktif akun. Berlaku untuk semua generate berikutnya (dari Studio, API, maupun web) sampai diganti lagi.

POST /api/v1/ai/model X-API-Token: rnv4_xxxxxxxxxxxx Content-Type: application/json { "model_key": "gemini-3.1-flash-image" } # Response { "ok": true, "model_key": "gemini-3.1-flash-image" }
GET /api/v1/avatars

Daftar avatar yang tersedia — publik, tidak perlu X-API-Token. Gunakan untuk menampilkan ikon profil pengguna (mis. di RNV4 Studio) berdasarkan avatar_id dari /api/v1/ai/profile atau /api/v1/user/me.

Penting untuk aplikasi native (RNV4 Studio dkk): jangan render karakter emoji di field e langsung — aplikasi desktop umumnya tidak punya font emoji terpasang, hasilnya kotak kosong. Gunakan icon_url (URL gambar PNG absolut) dan tampilkan sebagai gambar biasa (mis. Image/PictureBox control). Field ini yang dipakai web (browser) juga, supaya tampilan konsisten di semua platform.
avatar_id 0Tampilkan inisial huruf pertama username, bukan gambar dari array ini
avatar_id NAmbil avatars[N] langsung (indeks array = avatar_id, bukan N-1)
{ "ok": true, "avatars": [ { "e": "🐱", "c": "#f97316", "icon_url": "https://app.ruskomponen.uk/static/img/avatars/0.png" }, { "e": "🐶", "c": "#eab308", "icon_url": "https://app.ruskomponen.uk/static/img/avatars/1.png" }, { "e": "🐸", "c": "#22c55e", "icon_url": "https://app.ruskomponen.uk/static/img/avatars/2.png" } // ... total 20 entri, indeks 0..19 ] }

icon_url = URL gambar PNG (72×72px, rekomendasi utama), c = warna hex latar lingkaran, e = karakter emoji (khusus web, opsional). Contoh render: avatar_id=5avatars[5] → gambar penguin, warna latar #3b82f6.

GET /api/v1/ai/robots

Daftar robot yang terhubung ke akun beserta status online real-time. Gunakan untuk menampilkan indikator koneksi robot di aplikasi Anda.

onlinetrue jika robot mengirim heartbeat dalam 15 menit terakhir
firmwareVersi firmware aktif robot, misal v1.9.35
stateState mesin robot: 0 idle, 1 ready, 2 working
{ "robots": [ { "robot_id": "RNV4-A1B2C3", "online": true, "firmware": "v1.9.35", "state": 1, "claimed_at": "2026-01-15 08:00:00" }, { "robot_id": "RNV4-D4E5F6", "online": false, "firmware": "v1.9.34", "state": 0, "claimed_at": "2026-03-01 10:30:00" } ] }
GET /api/v1/credit/topup/{topup_id}/status

Poll status pembayaran topup. Gunakan ini jika tidak menggunakan webhook topup.success. topup_id didapat dari response saat membuat topup.

pendingMenunggu pembayaran / bukti transfer
reviewingBukti sedang diverifikasi admin
confirmedDikonfirmasi — kredit sudah ditambah
failedPembayaran gagal di gateway (Tripay)
expiredBatas waktu pembayaran habis
refundedDana dikembalikan — kredit dipotong kembali
rejectedDitolak admin (manual topup)
{ "ok": true, "topup_id": 5, "status": "paid", "credits": 55, "amount_idr": 50000, "paid_at": "2026-06-25 10:00:00" }
GET /api/v1/credit/topup/by-ref/{payment_ref}/status

Cek status topup berdasarkan payment_ref (merchant reference Tripay) — berguna di halaman return URL setelah redirect dari DANA, OVO, atau metode redirect lainnya, di mana Anda tidak menyimpan topup_id secara lokal.

GET /api/v1/credit/topup/by-ref/RNV4-20260625-0005/status { "ok": true, "status": "confirmed", "credits": 55, "amount_idr": 50000, "paid_at": "2026-06-25 10:00:00" }
POST /rnv4/share

Upload foto (misalnya hasil /api/v1/ai/generate atau foto apa pun) dan dapatkan link share publik yang bisa di-scan pelanggan via QR code. Masa aktif link diatur admin (5 menit s/d 72 jam, atau Unlimited/tanpa batas waktu) — jangan asumsikan durasi tetap, selalu baca dari field response di bawah.

Request (multipart/form-data)

FieldTipeKeterangan
filefile File gambar, maks 5MB wajib. Selalu disajikan ulang sebagai image/png apa pun format aslinya.

Header X-API-Token

Sertakan header ini agar upload tercatat sebagai milik akun Anda — foto akan muncul di Share History dan memicu webhook share.created. Beberapa saat admin dapat mengaktifkan pengaturan yang mewajibkan token ini (upload tanpa token/token tidak valid ditolak 401) — selalu sertakan token agar integrasi Anda tidak terpengaruh perubahan pengaturan tersebut.

curl -X POST https://app.ruskomponen.uk/rnv4/share \ -H "X-API-Token: rnv4-your-token-here" \ -F "[email protected]" # Respons saat admin set masa aktif TERBATAS (mis. 12 jam) { "url": "https://ruskomponen.uk/share/aBcDeFgH1234", "token": "aBcDeFgH1234", "expires_h": 12, "expires_m": 720, "unlimited": false } # Respons saat admin set "Unlimited" — perhatikan expires_h bernilai null, BUKAN angka { "url": "https://ruskomponen.uk/share/aBcDeFgH1234", "token": "aBcDeFgH1234", "expires_h": null, "expires_m": 0, "unlimited": true }
Selalu cek field unlimited sebelum memakai expires_h/expires_m — kalau true, kedua field itu bukan durasi nyata (expires_h=null, expires_m=0), bukan berarti "sudah kedaluwarsa".

Error

400bad_requestField file tidak ada, kosong, atau melebihi 5MB
401unauthorizedToken wajib (diaktifkan admin) tapi tidak dikirim / tidak valid
GET /api/v1/share/history

Daftar foto yang di-share milik akun Anda (paginasi 20 per halaman). Mendukung auth JWT (browser) maupun X-API-Token (robot/aplikasi).

GET /api/v1/share/history?page=1 X-API-Token: rnv4_xxxxxxxxxxxx { "ok": true, "total": 12, "page": 1, "per_page": 20, "items": [ { "token": "aBcDeFgH1234", "url": "https://ruskomponen.uk/share/aBcDeFgH1234", "image_url": "https://ruskomponen.uk/share/aBcDeFgH1234/image", "qr_url": "https://ruskomponen.uk/share/aBcDeFgH1234/qr.png", "upload_time": "2026-06-26T10:30:00+00:00", "expires_at": "2026-06-27T10:30:00+00:00", "is_unlimited": false, "is_expired": false } ] }
DELETE /api/v1/share/{token}

Hapus foto share milik Anda. Hanya pemilik (akun yang mengupload) yang bisa menghapus.

DELETE /api/v1/share/aBcDeFgH1234 X-API-Token: rnv4_xxxxxxxxxxxx { "ok": true }
Polling: Panggil /api/v1/share/history secara berkala (misalnya setiap 30 detik) untuk memperbarui daftar foto di aplikasi Anda. Atau gunakan webhook share.created untuk notifikasi real-time setiap kali foto baru di-share.

Endpoint tambahan (publik, tanpa auth):

GET /share/{token}/qr.png → QR code PNG (200×200) GET /share/{token}/image → foto PNG asli GET /share/{token} → halaman web untuk pelanggan scan

Webhook

Setup Webhook

Webhook memungkinkan server mengirim notifikasi real-time ke URL Anda saat event tertentu terjadi, tanpa perlu polling.

  1. 1Di Dashboard → tab Developer → bagian Webhook → klik Tambah
  2. 2Masukkan URL endpoint yang akan menerima POST request dari server kami
  3. 3Pilih events yang ingin Anda terima
  4. 4Simpan Webhook Secret yang muncul — hanya ditampilkan sekali, dipakai untuk verifikasi signature
  5. 5Klik Test untuk kirim payload percobaan ke URL Anda
Endpoint Anda harus merespons HTTP 2xx dalam waktu 10 detik. Jika gagal, server akan retry hingga 3 kali (delay: 10 detik → 60 detik → 300 detik).

Events

ai.generate.processing proses

Gemini mulai memproses gambar. Gunakan untuk menampilkan indikator "sedang diproses..." tanpa perlu poll status.

ai.generate.done sukses

Generate gambar AI selesai dan hasil siap diunduh.

ai.generate.failed gagal

Generate gagal (timeout Gemini, error API, dll). Kredit dikembalikan otomatis.

topup.success sukses

Topup kredit berhasil dikonfirmasi (Tripay atau manual). Kredit sudah ditambah ke saldo.

topup.pending menunggu

Topup baru dibuat — menunggu pembayaran. Berguna untuk menampilkan notifikasi "pembayaran sedang diproses" di aplikasi Anda.

topup.reviewing review

Bukti transfer manual berhasil diunggah user — sedang menunggu konfirmasi admin. Gunakan ini untuk menampilkan status "bukti sedang diverifikasi".

topup.failed gagal

Pembayaran Tripay gagal di sisi gateway (kartu ditolak, saldo tidak cukup, dll). Kredit tidak ditambahkan.

topup.expired kadaluarsa

Batas waktu pembayaran Tripay habis (default 24 jam). User perlu membuat topup baru.

topup.refunded refund

Penting: Dana dikembalikan oleh Tripay setelah sebelumnya PAID. Kredit yang sudah ditambahkan akan dipotong kembali dari saldo. Tangani event ini untuk memperbarui tampilan saldo di aplikasi Anda.

topup.rejected ditolak

Topup ditolak oleh admin, disertai alasan penolakan. Kredit tidak ditambahkan.

credit.low peringatan

Saldo kredit turun di bawah threshold setelah generate selesai. Gunakan ini untuk memperingatkan user agar topup sebelum kehabisan. Threshold default: 5 kredit.

kiosk.payment.paid kios

Pembayaran QRIS kios (lihat Kios QRIS) lunas — alternatif buat integrator yang tidak mau polling GET /api/v1/kiosk/checkout/<id>/status terus-menerus. payment_ref di payload ini sama persis dengan yang dikembalikan saat POST /checkout dan saat polling status.

{ "event": "kiosk.payment.paid", "user_id": 42, "data": { "transaction_id": 7, "payment_ref": "TRY-KSK-A1B2C3D4", "robot_id": "APP-001", "amount_idr": 15000, "fee_idr": 750, "net_idr": 14250, "balance_after": 128450, "paid_at": "2026-07-31 12:25:07" }, "timestamp": "2026-07-31T12:25:07Z" }
kiosk.payment.failed gagal

Wajib ditangani: pembayaran ditolak gateway (mis. saldo e-wallet pelanggan tidak cukup). Klien harus reset UI kios ke layar awal begitu event ini diterima (lihat catatan lengkap di Poll Status Transaksi). Tidak ada saldo yang berpindah (transaksi belum pernah paid).

{ "event": "kiosk.payment.failed", "user_id": 42, "data": { "transaction_id": 7, "payment_ref": "TRY-KSK-A1B2C3D4", "robot_id": "APP-001", "amount_idr": 15000 }, "timestamp": "2026-07-31T12:25:07Z" }
kiosk.payment.expired kadaluarsa

Wajib ditangani: masa berlaku ASLI QRIS di Tripay habis tanpa dibayar. Klien harus reset UI kios ke layar awal begitu event ini diterima (lihat catatan lengkap di Poll Status Transaksi). Tidak ada saldo yang berpindah (transaksi belum pernah paid).

{ "event": "kiosk.payment.expired", "user_id": 42, "data": { "transaction_id": 7, "payment_ref": "TRY-KSK-A1B2C3D4", "robot_id": "APP-001", "amount_idr": 15000 }, "timestamp": "2026-07-31T12:25:07Z" }
kiosk.payment.refunded refund

Penting: Tripay mengembalikan dana SETELAH sebelumnya paid — jarang terjadi (bukan alur normal), tapi kalau ini muncul, saldo yang sebelumnya masuk Saldo pemilik robot sudah ditarik kembali otomatis di server. Event ini murni informasional untuk klien kios (transaksi ini biasanya sudah lama tidak lagi ditampilkan/dipantau di layar QRIS) — tidak perlu aksi UI khusus selain, kalau relevan, mencatatnya untuk rekonsiliasi internal Anda sendiri.

{ "event": "kiosk.payment.refunded", "user_id": 42, "data": { "transaction_id": 7, "payment_ref": "TRY-KSK-A1B2C3D4", "robot_id": "APP-001", "amount_idr": 15000 }, "timestamp": "2026-07-31T12:25:07Z" }
share.created share

Foto berhasil di-share oleh robot menggunakan API Key. Payload berisi URL share dan URL QR code yang siap ditampilkan di aplikasi Anda — tanpa perlu polling history.

{ "event": "share.created", "user_id": 42, "data": { "token": "aBcDeFgH1234", "url": "https://ruskomponen.uk/share/aBcDeFgH1234", "qr_url": "https://ruskomponen.uk/share/aBcDeFgH1234/qr.png", "expires_at": "2026-06-27T10:30:00+00:00", "unlimited": false }, "timestamp": "2026-06-26T10:30:00Z" }

Format Payload

Server mengirim POST ke URL Anda dengan header dan body berikut:

Headers

Content-Typeapplication/json
X-RNV4-Signaturesha256=<hmac> — HMAC-SHA256 dari body menggunakan webhook secret
X-RNV4-EventNama event, misal ai.generate.done
X-RNV4-DeliveryID unik delivery ini (hex string)
X-RNV4-TimestampUnix timestamp saat pengiriman

Body — ai.generate.processing

{ "event": "ai.generate.processing", "timestamp": "2026-06-25T10:05:00Z", "delivery_id": "f6a7b8c9", "data": { "gen_id": 42, "prompt": "Ubah foto ini menjadi line art" } }

Body — ai.generate.done

{ "event": "ai.generate.done", "timestamp": "2026-06-25T10:05:30Z", "delivery_id": "a1b2c3d4", "data": { "gen_id": 42, "robot_id": "RNV4-001", "prompt": "Ubah foto ini menjadi line art", "result_url": "https://app.ruskomponen.uk/api/v1/ai/result/42?token=xxx", "credits_used": 1, "model": "gemini-2.5-flash-image", "done_at": "2026-06-25T10:05:30Z" } }

Body — ai.generate.failed

{ "event": "ai.generate.failed", "timestamp": "2026-06-25T10:05:30Z", "delivery_id": "b2c3d4e5", "data": { "gen_id": 42, "prompt": "Ubah foto ini menjadi line art", "error": "Gemini API timeout after 30s" } }

Body — topup.success

{ "event": "topup.success", "timestamp": "2026-06-25T10:00:00Z", "delivery_id": "c3d4e5f6", "data": { "topup_id": 5, "payment_ref": "TRP1A2B3C4D5", "credits": 55, "amount_idr": 50000, "gateway": "tripay", "paid_at": "2026-06-25T10:00:00Z" } }

Body — topup.pending

{ "event": "topup.pending", "timestamp": "2026-06-25T09:59:00Z", "delivery_id": "d4e5f6a7", "data": { "topup_id": 5, "credits": 55, "amount_idr": 50000, "method": "QRIS", "status": "pending", "created_at": "2026-06-25 09:59:00" } }

Body — topup.reviewing

{ "event": "topup.reviewing", "timestamp": "2026-06-25T10:05:00Z", "delivery_id": "h8i9j0k1", "data": { "topup_id": 5, "payment_ref": "RNV4-20260625-0005", "credits": 55, "amount_idr": 50000, "submitted_at": "2026-06-25 10:05:00" } }

Body — topup.failed

{ "event": "topup.failed", "timestamp": "2026-06-25T10:10:00Z", "delivery_id": "i9j0k1l2", "data": { "topup_id": 5, "payment_ref": "TRP1A2B3C4D5", "credits": 55, "amount_idr": 50000, "method": "QRIS", "failed_at": "2026-06-25 10:10:00" } }

Body — topup.expired

{ "event": "topup.expired", "timestamp": "2026-06-26T09:59:00Z", "delivery_id": "j0k1l2m3", "data": { "topup_id": 5, "payment_ref": "TRP1A2B3C4D5", "credits": 55, "amount_idr": 50000, "method": "QRIS", "expired_at": "2026-06-26 09:59:00" } }

Body — topup.refunded

Perhatian: Kredit sudah dipotong kembali dari saldo saat event ini dikirim. Perbarui tampilan saldo di aplikasi Anda.
{ "event": "topup.refunded", "timestamp": "2026-06-26T11:00:00Z", "delivery_id": "k1l2m3n4", "data": { "topup_id": 5, "payment_ref": "TRP1A2B3C4D5", "credits": 55, "amount_idr": 50000, "method": "QRIS", "refunded_at": "2026-06-26 11:00:00", "note": "Kredit telah dipotong kembali dari saldo akun." } }

Body — topup.rejected

{ "event": "topup.rejected", "timestamp": "2026-06-25T10:30:00Z", "delivery_id": "g7h8i9j0", "data": { "topup_id": 5, "credits": 55, "amount_idr": 50000, "reason": "Bukti transfer tidak sesuai", "rejected_at": "2026-06-25 10:30:00" } }

Body — credit.low

{ "event": "credit.low", "timestamp": "2026-06-25T10:05:31Z", "delivery_id": "e5f6a7b8", "data": { "balance": 3, "threshold": 5, "reason": "after_generate" } }

Body — share.created

{ "event": "share.created", "timestamp": "2026-06-26T10:30:00Z", "delivery_id": "f7a8b9c0", "data": { "token": "aBcDeFgH1234", "url": "https://ruskomponen.uk/share/aBcDeFgH1234", "qr_url": "https://ruskomponen.uk/share/aBcDeFgH1234/qr.png", "expires_at": "2026-06-27T10:30:00+00:00", "unlimited": false } }

Verifikasi Signature

Selalu verifikasi header X-RNV4-Signature untuk memastikan request berasal dari server kami dan tidak dimanipulasi. Signature adalah HMAC-SHA256 dari raw request body menggunakan webhook secret Anda.

HMAC-SHA256(key=webhook_secret, message=raw_body)
Penting: Gunakan raw body bytes untuk komputasi HMAC, bukan parsed JSON. Jangan pernah memproses request webhook tanpa verifikasi signature.

Contoh Kode

Flask webhook receiver + verifikasi signature

import hashlib, hmac, json from flask import Flask, request, jsonify app = Flask(__name__) WEBHOOK_SECRET = "webhook-secret-anda-di-sini" def verify_signature(raw_body: bytes, header_sig: str) -> bool: expected = "sha256=" + hmac.new( WEBHOOK_SECRET.encode(), raw_body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, header_sig) @app.route("/webhook", methods=["POST"]) def webhook(): sig = request.headers.get("X-RNV4-Signature", "") if not verify_signature(request.get_data(), sig): return jsonify({"error": "Invalid signature"}), 401 event = request.headers.get("X-RNV4-Event") data = request.json.get("data", {}) if event == "ai.generate.done": gen_id = data["gen_id"] result_url = data["result_url"] print(f"Generate #{gen_id} selesai: {result_url}") # Download gambar, simpan ke storage, notif user, dll elif event == "ai.generate.failed": print(f"Generate #{data['gen_id']} gagal: {data['error']}") elif event == "topup.success": print(f"Topup #{data['topup_id']} berhasil: {data['credits']} kredit") return jsonify({"ok": True})

Kirim generate request dari Python

import requests, time API_TOKEN = "rnv4-token-anda" BASE_URL = "https://app.ruskomponen.uk" # 1. Kirim foto with open("foto.jpg", "rb") as f: resp = requests.post( f"{BASE_URL}/api/v1/ai/generate", headers={"X-API-Token": API_TOKEN}, files={"photo": f}, data={"prompt": "Buat line art", "robot_id": "APP-001"}, ) gen = resp.json() gen_id = gen["gen_id"] print(f"Job {gen_id} queued, sisa kredit: {gen['balance']}") # 2. Poll status while True: s = requests.get( f"{BASE_URL}/api/v1/ai/status/{gen_id}", headers={"X-API-Token": API_TOKEN}, ).json() if s["status"] == "done": token = s["result_token"] # 3. Download hasil img = requests.get(f"{BASE_URL}/api/v1/ai/result/{gen_id}?token={token}") with open("hasil.png", "wb") as out: out.write(img.content) print("Selesai! hasil.png") break elif s["status"] == "failed": print("Gagal:", s.get("error_msg")) break time.sleep(3)

Kios QRIS

Mode kios adalah alur foto-booth walk-up: pelanggan tidak perlu akun Ruskomponen. Robot menampilkan QRIS seharga price_idr (harga jual per generate milik pemilik robot), pelanggan bayar, lalu generate dijalankan begitu pembayaran lunas.

Dua aliran uang yang independen — jangan disamakan: (1) QRIS pelanggan (dikurangi fee Tripay) masuk ke Saldo pemilik robot — ini pendapatan pemilik robot dari jasa foto yang ia jual ke pelanggannya. (2) Kredit pemilik robot tetap dipotong seperti generate personal biasa di POST /api/v1/ai/generate — ini biaya jasa AI yang selalu berlaku, apa pun sumber uang pelanggan. QRIS bukan pengganti kredit, cuma alat bantu terima pembayaran dari pelanggan.

Semua endpoint di bawah pakai X-API-Token milik pemilik robot (token yang sama dipakai untuk AI Doodle personal) — bukan token pelanggan, karena pelanggan tidak login.

Kios hanya aktif kalau lima syarat berurutan terpenuhi: (1) sakelar master admin ON secara global, (2) akun pemilik robot sudah diizinkan admin lewat allowlist (kurasi manual per-akun, default OFF), (3) toggle "Kios QRIS" di tab Pengaturan Pembayaran dashboard pemilik robot juga ON, (4) harga jual per generate sudah diatur minimal Rp 1.000 (batas minimum channel QRIS Tripay), dan (5) pemilik robot sudah mengisi nomor WA pengaduan (lihat complaint_wa_link di bawah). Kalau salah satu OFF/belum terpenuhi, GET /api/v1/kiosk/status mengembalikan available: false dan robot sebaiknya jatuh kembali ke alur AI Doodle personal biasa — endpoint ini tidak membedakan syarat mana yang gagal, cukup jadikan sinyal biner "tampilkan alur QRIS atau tidak". POST /checkout juga ditolak 402 kalau kredit pemilik robot sudah kurang dari biaya 1x generate — dicek di awal supaya pelanggan tidak terlanjur bayar QRIS lalu generate-nya gagal krn kredit habis.
GET /api/v1/kiosk/status

Cek apakah mode kios aktif untuk akun ini, dan berapa harga per foto saat ini. Panggil endpoint ini saat robot masuk mode kios (mis. layar utama foto-booth) untuk memutuskan menampilkan alur QRIS atau tidak.

{ "ok": true, "available": true, "price_idr": 15000, "complaint_wa_digits": "6281234567890", "complaint_wa_link": "https://wa.me/6281234567890" }
complaint_wa_link adalah nomor WhatsApp pemilik robot (bukan admin Ruskomponen — Ruskomponen bukan pihak dalam transaksi jasa foto walk-up ini). Simpan nilai ini di sesi kios saat status di-fetch, dipakai nanti kalau perlu menampilkan kontak pengaduan — lihat kontrak kapan menampilkannya di Poll Status Transaksi. null kalau available: false.
POST /api/v1/kiosk/checkout

Buat transaksi QRIS baru untuk 1x foto. Yang menentukan pemilik penghasilan adalah X-API-Token, bukan robot_id — robot tidak perlu diklaim/didaftarkan dulu ke akun. Masa berlaku QRIS ASLI (expires_at) mengikuti batas channel QRIS Tripay sendiri (biasanya puluhan menit) — tidak kami paksa lebih pendek. timeout_secs di response adalah saran terpisah kapan sebaiknya UI kios menyerah & kembali ke layar awal (independen dari expires_at) — pelanggan berikutnya cukup memicu POST /checkout baru walau transaksi lama belum benar-benar expired; transaksi lama dibiarkan expired sendiri di Tripay, tidak perlu ditangani apa pun.

Request (application/json)

FieldTipeKeterangan
robot_idstring ID robot yang dipakai kios, hanya untuk log/tracking (bukan otorisasi — pola sama seperti robot_id di Generate Gambar). Kosong = "unknown".

Response

{ "ok": true, "transaction": { "id": 123, "payment_ref": "TRY-KSK-A1B2C3D4", "amount_idr": 15000, "qr_url": "https://tripay.co.id/qr/xxxxx.png", "qr_string": "00020101...", "expires_at": 1785484855, "timeout_secs": 180, "status": "pending" } }

Tampilkan qr_url di layar robot, lalu poll status transaksi sampai paid. Kalau sampai timeout_secs (saran: 180 detik) belum paid, cukup reset UI ke layar awal di sisi klien — tidak perlu menunggu expired beneran dari server.

Kalau gagal dibuat (Tripay error/jaringan)

{ "ok": false, "error": "Gagal membuat transaksi." }
HTTP status non-200 (umumnya 502 — Tripay tidak bisa dihubungi/error di sisi mereka). Ini terjadi sebelum QR pernah ditampilkan ke pelanggan, jadi tidak perlu "kembali ke layar awal" — tidak ada transaksi/tampilan yang perlu di-reset. Cukup tampilkan pesan error singkat ke operator/pelanggan dan izinkan coba lagi (tekan tombol bayar sekali lagi memicu POST /checkout baru).
GET /api/v1/kiosk/checkout/{id}/status

Poll status transaksi QRIS kios. Disarankan interval 2-3 detik selama pelanggan masih di layar QRIS. Sebagai alternatif polling, lihat event webhook kiosk.payment.* — dikirim otomatis tiap kali status berubah, tidak perlu polling terus-menerus.

pendingMenunggu pembayaran — lanjutkan polling.
paidLunas — lanjutkan ke generate memakai id ini.
failedPembayaran ditolak gateway (mis. saldo e-wallet pelanggan tidak cukup). Klien HARUS reset UI ke layar awal begitu status ini terlihat — lihat catatan di bawah.
expiredMasa berlaku ASLI di Tripay habis tanpa dibayar. Klien HARUS reset UI ke layar awal begitu status ini terlihat — lihat catatan di bawah.
{ "ok": true, "id": 123, "status": "paid", "payment_ref": "TRY-KSK-A1B2C3D4" }
Server TIDAK BISA memaksa reset UI robot — ini API REST biasa, server cuma menjawab pertanyaan "apa status transaksi ini sekarang", tidak bisa mendorong perintah ke klien. Kalau klien berhenti polling begitu timeout_secs lewat (rekomendasi umum, lihat bagian Buat Transaksi QRIS) TANPA pernah mengecek status failed/expired lebih dulu, layar QRIS bisa nyangkut tampil terus walau transaksinya sendiri sudah mati di sisi server — ini bug di klien, bukan sesuatu yang server bisa perbaiki dari sisi mana pun. Pastikan loop polling klien memeriksa status di SETIAP respons dan memanggil fungsi reset-ke-layar-awal begitu ketemu failed atau expired — jangan cuma mengandalkan timer lokal klien sendiri.
Kasus tepi — pembayaran masuk SETELAH klien menyerah: kalau klien sudah reset ke layar awal (krn timeout_secs lewat / operator batal) tapi pelanggan ternyata TETAP menyelesaikan pembayaran QRIS-nya sesaat kemudian (mis. transfer sempat lambat), transaksi itu akan tetap berubah jadi paid di server & pendapatannya tetap masuk Saldo pemilik robot — tapi generate untuk pelanggan itu TIDAK PERNAH terjadi otomatis, krn klien sudah tidak polling/menampilkan transaksi itu lagi (pelanggan kemungkinan sudah pergi). Tidak ada cara klien "menangkap" kasus ini setelah UI-nya sendiri sudah reset — pemilik robot bisa melihat transaksi paid yang belum sempat menghasilkan foto lewat dashboard mereka (kolom Saldo, transaksi kios, ditandai otomatis "⏱ Lunas telat" kalau lunasnya lebih dari 3 menit sejak checkout dibuat) & memutuskan tindak lanjutnya sendiri (mis. refund manual kalau pelanggan komplain). Ini bukan bug untuk diperbaiki di klien — cukup dipahami sebagai batasan alur walk-up tanpa akun: begitu UI reset, transaksi itu di luar kendali sesi kios saat itu.
Kontrak popup kontak pengaduan (complaint_wa_link): pengaduan pelanggan soal pembayaran kios seharusnya ke pemilik robot, bukan admin Ruskomponen (Ruskomponen bukan pihak dalam transaksi jasa foto walk-up ini — lihat Syarat & Ketentuan). Rekomendasi kapan klien menampilkan popup/notifikasi berisi complaint_wa_link ke pelanggan:
  • Tampilkan begitu status failed atau expired terlihat saat polling — tepat sebelum/bersamaan reset UI ke layar awal, dengan hitung mundur singkat (mis. 5-10 detik) sebelum layar benar-benar kembali ke awal, supaya pelanggan sempat membaca nomor kontaknya.
  • Tampilkan begitu klien sendiri menyerah krn timeout_secs (3 menit) lewat TANPA status paid — ini kasus paling umum diadukan pelanggan ("saya sudah bayar tapi layar kembali ke awal"): uang mereka mungkin tetap masuk belakangan (lihat kasus tepi di atas), pemilik robot yang bisa mengecek & menindaklanjuti lewat dashboard-nya.
  • JANGAN tampilkan kalau status berubah paid dalam timeout_secs — alur normal lanjut langsung ke generate tanpa popup apa pun.
  • JANGAN tampilkan untuk kegagalan POST /checkout itu sendiri (sebelum QR pernah tampil, lihat catatan di atas) — belum ada uang pelanggan yang berpindah sama sekali, cukup pesan error biasa & izinkan coba lagi.
POST /api/v1/ai/generate

Endpoint generate sama persis dengan Generate Gambar biasa, termasuk tetap memotong kredit pemilik robot seperti biasa — cukup tambahkan 1 field form opsional untuk menandai generate ini dilatarbelakangi pembayaran QRIS kios (rekonsiliasi/riwayat). Sisa alur (polling status, ambil hasil, Webhook ai.generate.done) identik dengan alur personal.

Field tambahanTipeKeterangan
kiosk_transaction_idint id transaksi kios yang sudah paid. Murni penanda/pelacak (link generate ini ke pembayaran QRIS-nya) — tidak mengubah cara kredit dipotong.
1 transaksi kios hanya boleh jadi penanda utk 1x generate (cegah 1 pembayaran ditandai berkali-kali). Memakai kiosk_transaction_id yang sama dua kali (atau yang belum paid) akan ditolak. Terpisah dari itu, generate tetap bisa ditolak 402 kalau kredit pemilik robot memang tidak cukup — sama seperti alur personal.

Response

{ "gen_id": 99, "result_token": "a1b2c3d4e5f6...", "status": "pending", "queue_position": 0, "credit_cost": 1, "balance": 48 }

Referensi

Error Codes

HTTPKodeKeterangan
400bad_requestParameter tidak valid atau field wajib kosong
401unauthorizedAPI Token tidak valid atau tidak ada
402payment_requiredKredit tidak cukup untuk generate
403forbiddenResource ditemukan tapi bukan milik akun Anda (mis. hapus share orang lain)
404not_foundgen_id tidak ditemukan atau bukan milik akun Anda
410goneHasil sudah expired (>1 jam setelah selesai)
429rate_limitedTerlalu banyak request. Tunggu beberapa detik.

Rate Limits

Endpoint / AksiLimit per akun
POST /ai/generate1 request per interval (default: 10 detik). Cek nilai aktual via GET /api/v1/ai/rate-limit
GET /ai/status60 request / menit — cukup untuk poll tiap detik
GET /ai/queue30 request / menit
GET /ai/balance30 request / menit
GET /ai/profile20 request / menit
GET /ai/token-info20 request / menit
GET /ai/robots20 request / menit
GET /ai/rate-limit30 request / menit

Saat kena limit, server mengembalikan HTTP 429. Implementasikan exponential backoff — jangan langsung retry dalam loop ketat.

Keamanan

Panduan ini membantu Anda mengintegrasikan API dengan aman. Keamanan yang baik tidak bergantung pada informasi ini disembunyikan — justru transparansi membantu developer membangun integrasi yang benar.

Simpan API Key dengan Aman

  • Simpan di environment variable atau secrets manager, bukan hardcode di source code
  • Jangan commit key ke git — gunakan .env dan tambahkan ke .gitignore
  • Buat key terpisah per aplikasi — mudah di-revoke jika satu bocor
  • Jangan tampilkan key di log, error message, atau response ke client

Selalu Verifikasi Webhook Signature

Tanpa verifikasi, endpoint Anda bisa menerima request palsu dari siapa saja yang mengetahui URL Anda. Selalu validasi header X-RNV4-Signature sebelum memproses payload.

# Python — verifikasi wajib sebelum proses import hmac, hashlib def is_valid(secret: str, body: bytes, sig_header: str) -> bool: expected = "sha256=" + hmac.new( secret.encode(), body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, sig_header) # Tolak jika tidak valid if not is_valid(WEBHOOK_SECRET, request.data, request.headers.get("X-RNV4-Signature", "")): return "Forbidden", 403

Gunakan HTTPS

  • Semua request ke API harus via HTTPS — API key dikirim di header dan bisa dicuri jika pakai HTTP biasa
  • URL webhook Anda juga harus HTTPS agar payload tidak bisa disadap di jaringan

Respons Terhadap Rate Limit (429)

Jika menerima HTTP 429, hentikan request dan tunggu sebelum coba lagi. Jangan retry dalam loop tanpa jeda — ini tidak akan berhasil dan memperburuk situasi.

# Exponential backoff yang benar import time def poll_with_backoff(gen_id, api_key, max_tries=10): delay = 3 for _ in range(max_tries): r = requests.get(f"{BASE}/status/{gen_id}", headers={"X-API-Token": api_key}) if r.status_code == 429: time.sleep(delay) delay = min(delay * 2, 60) # maks 60 detik continue data = r.json() if data["status"] in ("done", "failed"): return data time.sleep(3) # interval normal polling return None

Sistem Kredit sebagai Perlindungan Ekonomi

Setiap generate memotong kredit akun — spam generate langsung merugikan pemilik akun sendiri. Jika kredit habis, server mengembalikan 402 Payment Required. Daftarkan event credit.low di webhook untuk mendapat peringatan sebelum kehabisan.

Temukan celah keamanan? Laporkan ke [email protected] sebelum mempublikasikannya. Kami menghargai responsible disclosure.
© 2026 Ruskomponen Beranda Dashboard Status Layanan FAQ