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
- 1Daftar akun di https://app.ruskomponen.uk/register
- 2Buat API Key di Dashboard → Developer → API Keys
- 3Kirim foto ke
POST /api/v1/ai/generatedengan headerX-API-Token: <token> - 4Poll
GET /api/v1/ai/status/{gen_id}hinggastatus == "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": "..." } }
REST API
/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)
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
}
/api/v1/ai/status/{gen_id}
Cek status satu job generate. Poll endpoint ini hingga status bernilai done atau failed.
Nilai status
pendingAntri, belum diprosesprocessingSedang diproses oleh GeminidoneSelesai — hasil siap diambilfailedGagal — kredit dikembalikan otomatisexpiredHasil 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"
}
/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"
}
]
}
/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.
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
/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..."
}
]
}
/api/v1/ai/balance
Cek saldo kredit dan informasi rate limit.
{
"ok": true,
"balance": 49,
"rate_limit_secs": 10,
"username": "johndoe"
}
/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 }
/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"
}
/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_id — 0 berarti tampilkan inisial huruf pertama username (bukan emoji). Untuk avatar_id lain, lihat GET /api/v1/avatars utk daftar emoji+warnanya.
/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."
}
]
}
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
/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" }
/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.
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 iniavatar_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=5 → avatars[5] → gambar penguin, warna latar #3b82f6.
/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 terakhirfirmwareVersi firmware aktif robot, misal v1.9.35stateState 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"
}
]
}
/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 transferreviewingBukti sedang diverifikasi adminconfirmedDikonfirmasi — kredit sudah ditambahfailedPembayaran gagal di gateway (Tripay)expiredBatas waktu pembayaran habisrefundedDana dikembalikan — kredit dipotong kembalirejectedDitolak admin (manual topup){
"ok": true,
"topup_id": 5,
"status": "paid",
"credits": 55,
"amount_idr": 50000,
"paid_at": "2026-06-25 10:00:00"
}
/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"
}
Webhook
Setup Webhook
Webhook memungkinkan server mengirim notifikasi real-time ke URL Anda saat event tertentu terjadi, tanpa perlu polling.
- 1Di Dashboard → tab Developer → bagian Webhook → klik Tambah
- 2Masukkan URL endpoint yang akan menerima POST request dari server kami
- 3Pilih events yang ingin Anda terima
- 4Simpan Webhook Secret yang muncul — hanya ditampilkan sekali, dipakai untuk verifikasi signature
- 5Klik Test untuk kirim payload percobaan ke URL Anda
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/jsonX-RNV4-Signaturesha256=<hmac> — HMAC-SHA256 dari body menggunakan webhook secretX-RNV4-EventNama event, misal ai.generate.doneX-RNV4-DeliveryID unik delivery ini (hex string)X-RNV4-TimestampUnix timestamp saat pengirimanBody — 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
{
"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)
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)
Express.js webhook receiver + verifikasi signature
const express = require('express');
const crypto = require('crypto');
const app = express();
const WEBHOOK_SECRET = 'webhook-secret-anda-di-sini';
// Pakai raw body untuk verifikasi signature
app.use('/webhook', express.raw({ type: 'application/json' }));
function verifySignature(rawBody, headerSig) {
const expected = 'sha256=' + crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(headerSig || '')
);
}
app.post('/webhook', (req, res) => {
const sig = req.headers['x-rnv4-signature'] || '';
if (!verifySignature(req.body, sig)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const payload = JSON.parse(req.body);
const event = payload.event;
const data = payload.data;
if (event === 'ai.generate.done') {
console.log('Generate selesai:', data.gen_id, data.result_url);
// fetch(data.result_url, ...) untuk download gambar
} else if (event === 'topup.success') {
console.log('Topup berhasil:', data.credits, 'kredit');
}
res.json({ ok: true });
});
app.listen(3000, () => console.log('Webhook server running on :3000'));
PHP webhook receiver
<?php
$WEBHOOK_SECRET = 'webhook-secret-anda-di-sini';
// Baca raw body
$rawBody = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_RNV4_SIGNATURE'] ?? '';
$event = $_SERVER['HTTP_X_RNV4_EVENT'] ?? '';
// Verifikasi signature
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $WEBHOOK_SECRET);
if (!hash_equals($expected, $sig)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
$payload = json_decode($rawBody, true);
$data = $payload['data'] ?? [];
if ($event === 'ai.generate.done') {
$genId = $data['gen_id'];
$resultUrl = $data['result_url'];
// Download gambar: file_get_contents($resultUrl)
error_log("Generate #$genId selesai: $resultUrl");
} elseif ($event === 'topup.success') {
error_log("Topup berhasil: {$data['credits']} kredit");
}
http_response_code(200);
echo json_encode(['ok' => true]);
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.
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.
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.
/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.
/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)
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." }
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).
/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" }
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.
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.
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
failedatauexpiredterlihat 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 statuspaid— 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
paiddalamtimeout_secs— alur normal lanjut langsung ke generate tanpa popup apa pun. - JANGAN tampilkan untuk kegagalan
POST /checkoutitu sendiri (sebelum QR pernah tampil, lihat catatan di atas) — belum ada uang pelanggan yang berpindah sama sekali, cukup pesan error biasa & izinkan coba lagi.
/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.
kiosk_transaction_idint
id transaksi kios yang sudah paid. Murni penanda/pelacak (link generate ini ke pembayaran QRIS-nya) — tidak mengubah cara kredit dipotong.
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
bad_requestParameter tidak valid atau field wajib kosongunauthorizedAPI Token tidak valid atau tidak adapayment_requiredKredit tidak cukup untuk generateforbiddenResource ditemukan tapi bukan milik akun Anda (mis. hapus share orang lain)not_foundgen_id tidak ditemukan atau bukan milik akun AndagoneHasil sudah expired (>1 jam setelah selesai)rate_limitedTerlalu banyak request. Tunggu beberapa detik.Rate Limits
POST /ai/generate1 request per interval (default: 10 detik). Cek nilai aktual via GET /api/v1/ai/rate-limitGET /ai/status60 request / menit — cukup untuk poll tiap detikGET /ai/queue30 request / menitGET /ai/balance30 request / menitGET /ai/profile20 request / menitGET /ai/token-info20 request / menitGET /ai/robots20 request / menitGET /ai/rate-limit30 request / menitSaat 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
.envdan 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.