API read-only untuk memonitor jaringan FTTH/GPON dari aplikasi lain (web app lain, aplikasi Android, backend billing, dsb). Cukup panggil endpoint di bawah, kirim token, dan baca hasilnya dalam format JSON.
⛔ STATUS: API DINONAKTIFKAN (default, demi keamanan selama belum dipakai). Saklarnya
$apiEnableddiroutes/api.php. Untuk mengaktifkan: ubah$apiEnabled = true, lalusudo systemctl reload php8.3-fpm. Selama mati, semua/api/*membalas404dan tab Pengaturan → API & Token menampilkan peringatan + tombol "Buat Token" dinonaktifkan.
Sifat API ini: endpoint baca mengambil snapshot polling terakhir yang tersimpan di server — cepat dan tidak menyentuh OLT. Aksi tulis (register, reboot, rename, hapus ONU, refresh live) mengeksekusi Telnet/SNMP sinkron ke OLT dan di-gate role
admin/operator/partner(lihat §5).
- Base URL (produksi):
https://nms.kusumavision.net/api/v1 - Base URL (lokal dev):
http://localhost:8000/api/v1 - Format: JSON (
Content-Type: application/json) - Zona waktu timestamp: ISO-8601 (mis.
2026-06-28T10:15:30+07:00)
Bila ingin menempelkan widget status di halaman web lain tanpa proses login,
pakai endpoint publik ini. CORS sudah aktif untuk api/* (bisa dipanggil langsung
dari browser/JavaScript di domain mana pun).
GET /api/v1/public/status (tanpa token)
⚠️ Demi privasi, endpoint publik HANYA mengembalikan angka agregat (jumlah OLT/ONU online-offline, alarm aktif, status per-OLT). Ia tidak memuat data pelanggan (nama/alamat/serial) maupun IP OLT. Untuk data rinci ONU pelanggan, gunakan endpoint ber-token di bagian berikutnya — jangan pernah menaruh data pelanggan di halaman publik.Hasil di-cache 30 detik di server.
curl https://nms.kusumavision.net/api/v1/public/status{
"data": {
"olt": { "total": 2, "online": 2, "offline": 0 },
"onu": { "total": 480, "online": 472, "offline": 8 },
"online_share": 98.3,
"alarms": { "active": 3 },
"olts": [
{ "name": "OLT-C320-PATI", "reachable": true, "onu_total": 240, "onu_online": 236, "onu_offline": 4, "last_polled_at": "2026-06-28T10:14:00+07:00" }
]
},
"meta": { "generated_at": "2026-06-28T10:15:30+07:00" }
}Tempel potongan ini di halaman mana pun — ia menampilkan ringkasan status dan menyegarkan tiap 60 detik:
<div id="kv-status">Memuat status jaringan…</div>
<script>
(async function () {
const BASE = "https://nms.kusumavision.net/api/v1";
const el = document.getElementById("kv-status");
async function render() {
try {
const r = await fetch(`${BASE}/public/status`);
const { data } = await r.json();
el.innerHTML = `
<strong>Status Jaringan</strong><br>
OLT online: ${data.olt.online}/${data.olt.total} |
ONU online: ${data.onu.online}/${data.onu.total} (${data.online_share}%) |
Alarm aktif: ${data.alarms.active}
`;
} catch (e) {
el.textContent = "Gagal memuat status.";
}
}
render();
setInterval(render, 60000);
})();
</script>Catatan: karena dipanggil dari browser, endpoint ini terbuka untuk publik. Itu sebabnya isinya sengaja dibatasi ke angka agregat. Bila Anda butuh membatasi akses (mis. hanya domain tertentu), itu hanya bisa ditegakkan dari sisi server (panggil API lewat backend web lain memakai token, lalu sajikan hasilnya), bukan dari JavaScript di browser.
API memakai Bearer token (Laravel Sanctum / personal access token). Alurnya: login sekali untuk dapat token → simpan token → kirim token di setiap request berikutnya lewat header:
Authorization: Bearer <TOKEN>
Accept: application/json
POST /api/v1/auth/login (tanpa token — endpoint publik)
Body (JSON):
| Field | Wajib | Keterangan |
|---|---|---|
email |
ya | Email akun NMS |
password |
ya | Kata sandi akun |
device_name |
tidak | Label perangkat (mis. "Android - Budi"). Untuk identifikasi token. |
Contoh (curl):
curl -X POST https://nms.bmkv.net/api/v1/auth/login \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"email":"admin@bmkv.net","password":"rahasia","device_name":"Android - Budi"}'Respons 200:
{
"data": {
"token": "12|aBcD3Fg...XyZ",
"token_type": "Bearer",
"user": {
"id": 1,
"name": "Administrator",
"email": "admin@bmkv.net",
"role": "admin",
"role_label": "Administrator",
"is_admin": true,
"is_demo": false
}
}
}Simpan
data.tokendi sisi klien (mis.EncryptedSharedPreferencesdi Android, atau cookie httpOnly / secret store di backend web lain). Token hanya ditampilkan sekali.
Kredensial salah → 422:
{ "message": "Email atau kata sandi salah.", "errors": { "email": ["Email atau kata sandi salah."] } }Cara termudah — lewat UI: masuk sebagai admin → Pengaturan → tab "API & Token" → isi nama token → Buat Token. Token tampil sekali; salin dan simpan. Di tab itu juga bisa melihat & mencabut token kapan saja.
Atau lewat command (untuk backend tanpa UI / otomasi):
php artisan api:token admin@bmkv.net --name="Billing App"Output mencetak token sekali. Pakai sebagai Authorization: Bearer <token>.
GET /api/v1/me → mengembalikan objek user yang sama seperti pada login.
POST /api/v1/auth/logout → menghapus token yang sedang dipakai. Token tak lagi valid.
{ "data": { "message": "Token dicabut." } }- Sukses selalu dibungkus
{"data": ...}. Daftar yang dipaginasi menambah{"meta": ...}. - Error memakai format Laravel standar:
{"message": "...", "errors": {...}}(errors hanya untuk validasi). - Semua endpoint selain login butuh header
Authorization. - Rate limit: 120 request / menit per token. Header respons:
X-RateLimit-Limit,X-RateLimit-Remaining. Lewat batas →429. - Scoping demo: akun ber-role
demohanya melihat data demo; akun nyata melihat data nyata.
| Kode | Arti |
|---|---|
200 |
OK |
401 |
Token tidak ada / tidak valid ({"message":"Unauthenticated."}) |
404 |
Resource tidak ditemukan |
422 |
Validasi gagal (cek errors) |
429 |
Terlalu banyak request (rate limit) |
500 |
Kesalahan server |
Ringkasan:
| Method | Path | Fungsi |
|---|---|---|
| GET | /public/status |
Status agregat, tanpa token (embed) |
| POST | /auth/login |
Login, dapatkan token |
| GET | /me |
Info user token |
| POST | /auth/logout |
Cabut token |
| GET | /summary |
Ringkasan dashboard (counter) |
| GET | /olts |
Daftar OLT + status |
| GET | /olts/{olt} |
Detail 1 OLT (system, port, ONU) |
| GET | /onus |
Daftar ONU lintas-OLT (filter+paginasi) |
| GET | /olts/{olt}/onus/{slot}/{port}/{onuId} |
Detail 1 ONU |
| GET | /olts/{olt}/ports/{slot}/{port}/onus |
Daftar ONU 1 PON port (aplikasi mobile) |
| GET | /olts/{olt}/unconfigured |
ONU unconfigured (autofind, ZTE) |
| GET | /olts/{olt}/register/options |
Profil + default form registrasi ONU |
| GET | /search?q= |
Pencarian global OLT + ONU |
| GET | /alarms |
Daftar alarm |
| GET | /odps |
Daftar ODP (+ jumlah ONU) |
| GET | /odps/{odp} |
Detail 1 ODP |
| GET | /odps/{odp}/onus |
ONU di dalam sebuah ODP |
| GET | /map |
Pin ONU + pin ODP untuk peta |
| POST | /devices |
Daftarkan token FCM (push Android) |
| DELETE | /devices |
Cabut token FCM |
Aksi tulis (butuh role admin/operator; user demo diblokir — 403):
| Method | Path | Fungsi |
|---|---|---|
| POST | /olts/{olt}/register/preview |
Preview script CLI (tanpa OLT) |
| POST | /olts/{olt}/register |
Registrasi ONU (execute bool) |
| POST | /olts/{olt}/unconfigured/refresh |
Discovery unconfigured live |
| POST | /olts/{olt}/ports/{slot}/{port}/refresh |
Re-scan ONU 1 port (live SNMP) |
| POST | /olts/{olt}/onus/{slot}/{port}/{onuId}/reboot |
Reboot ONU |
| POST | /olts/{olt}/onus/{slot}/{port}/{onuId}/name |
Ubah nama/deskripsi ONU |
| DELETE | /olts/{olt}/onus/{slot}/{port}/{onuId} |
Hapus (deregister) ONU dari OLT |
Contoh hapus ONU (destruktif — deregistrasi permanen dari OLT; gated capability
supports_onu_delete):
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
https://nms.kusumavision.net/api/v1/olts/1/onus/1/2/5
# → { "data": { "ok": true, "message": "ONU 5 dihapus dari OLT.", "error": null } }Catatan mobile: endpoint baca
/search,/olts/{olt}/ports/.../onus,/olts/{olt}/unconfigured,/olts/{olt}/register/options,/odps*,/map+ aksi tulis di atas ditambahkan untuk aplikasi Android (mobile/)./olts/{olt}/register/optionsmenyertakan blokodps(seluruh ODP OLT itu lengkapslot/port) — klien menyaringnya per PON port dan mengirim balikodp_idopsional diPOST /olts/{olt}/register; kaitan ODP dibuat setelah CLI sukses, kegagalannya muncul sebagaidata.odp_errortanpa membatalkan registrasi.GET /olts/{olt}kini menyertakancapabilities(mis.supports_provisioning,supports_reboot,supports_onu_delete) agar klien menampilkan/menyembunyikan aksi per-driver. Registrasi ONU & refresh live ZTE-only (mode dasar); reboot/rename/delete bercabang per-family (ZTE, C-Data EPON/GPON, HiOSO) — perintah CLI menyesuaikan vendor OLT-nya. Aksi write mengeksekusi Telnet/SNMP sinkron (timeout klien ~120 dtk). Push FCM: lihat §7.
Aplikasi mendaftarkan token perangkat via POST /devices setelah login. Saat AlarmEvaluator
menaikkan/menurunkan alarm, server men-dispatch job (SendFcmAlarmNotifications) yang mengirim
push ke semua token (filter minimal severity FCM_MIN_SEVERITY, default major). Aktif setelah
service-account JSON dipasang di storage/app/firebase/service-account.json + FIREBASE_CREDENTIALS
di .env; tanpa itu fitur dormant (tak memengaruhi polling).
curl https://nms.bmkv.net/api/v1/summary \
-H "Authorization: Bearer $TOKEN" -H "Accept: application/json"{
"data": {
"olt": { "total": 2, "online": 2, "offline": 0 },
"onu": { "total": 480, "online": 472, "offline": 8, "warning": 5 },
"online_share": 98.3,
"alarms": { "total": 3, "critical": 1, "major": 1, "minor": 0, "warning": 1 }
},
"meta": { "generated_at": "2026-06-28T10:15:30+07:00" }
}{
"data": [
{
"id": 1,
"name": "OLT-C320-PATI",
"ip": "10.10.0.1",
"vendor": "ZTE",
"driver": "zte",
"is_cdata": false,
"reachable": true,
"polling_enabled": true,
"ports_total": 16,
"ports_up": 8,
"ports_down": 8,
"onu_total": 240,
"onu_online": 236,
"onu_offline": 4,
"last_polled_at": "2026-06-28T10:14:00+07:00",
"last_tested_at": "2026-06-27T09:00:00+07:00"
}
]
}driver salah satu dari: zte, cdata_epon, cdata_gpon, hioso_epon, unknown.
{olt} = id OLT. Mengembalikan field ringkasan (sama seperti di atas) plus
system dan ports:
{
"data": {
"id": 1,
"name": "OLT-C320-PATI",
"...": "(field ringkasan seperti pada GET /olts)",
"system": {
"sys_name": "OLT-C320-PATI",
"sys_descr": "ZTE ZXA10 C320 ...",
"sys_object_id": "1.3.6.1.4.1.3902...",
"sys_uptime": "12:34:56:00"
},
"ports": [
{
"if_index": 285278209,
"name": "gpon-olt_1/1/1",
"description": "KETANEN LAMA",
"slot": 1,
"port": 1,
"oper_status": "up",
"onu_total": 32,
"onu_online": 31
}
]
}
}OLT tak ada → 404.
description = deskripsi port PON (mis. nama area) hasil parse CLI show interface
(tabel smartolt_interface_statuses) — null bila belum pernah ditarik; khusus C600
fallback ke ifDescr SNMP.
Endpoint paling berguna untuk aplikasi monitoring pelanggan.
Query params:
| Param | Default | Keterangan |
|---|---|---|
olt_id |
semua | Saring 1 OLT saja |
status |
semua | online | offline | warning (online tapi RX di luar -25…-10 dBm) |
q |
— | Cari di SN, MAC, nama, deskripsi, nama pelanggan, interface, nama OLT |
page |
1 |
Halaman |
per_page |
50 |
Item per halaman (maks 200) |
curl "https://nms.bmkv.net/api/v1/onus?status=offline&per_page=20" \
-H "Authorization: Bearer $TOKEN" -H "Accept: application/json"{
"data": [
{
"olt_id": 1,
"olt_name": "OLT-C320-PATI",
"olt_cdata": false,
"slot": 1,
"port": 1,
"onu_id": 5,
"if_index": 285278209,
"interface": "gpon-onu_1/1/1:5",
"serial_number": "ZTEGC1234567",
"mac": null,
"type_name": "ZTE-F660",
"name": "Budi Santoso",
"description": "Jl. Merdeka 10",
"customer_name": "Budi Santoso",
"admin_state": "enable",
"phase_state": "Working",
"online": true,
"last_down_cause": null,
"rx_power_dbm": -21.5,
"rx_power_label": "-21.5 dBm"
}
],
"meta": { "total": 8, "per_page": 20, "current_page": 1, "last_page": 1, "count": 8 }
}curl "https://nms.bmkv.net/api/v1/olts/1/onus/1/1/5" \
-H "Authorization: Bearer $TOKEN" -H "Accept: application/json"Mengembalikan satu objek ONU (bentuk sama seperti elemen data pada /onus)
di dalam {"data": {...}}, termasuk odp_id + odp_name bila ONU itu sudah
dikaitkan ke sebuah ODP. Tidak ditemukan → 404.
ODP (Optical Distribution Point) = splitter lapangan; satu ODP terkunci ke satu OLT + satu PON port. Partner hanya melihat ODP milik OLT yang di-assign padanya.
Query params: olt_id, slot, port, q (cari nama ODP / nama OLT / catatan).
curl "https://nms.bmkv.net/api/v1/odps?olt_id=2" \
-H "Authorization: Bearer $TOKEN" -H "Accept: application/json"{
"data": [
{
"id": 26, "snmp_olt_id": 2, "olt_name": "OLT-C300-SEKARJALAK",
"name": "ODP BANGPE", "slot": 2, "port": 3,
"latitude": -6.6129883, "longitude": 111.0610271,
"notes": null, "onu_count": 6
}
],
"meta": { "count": 22 }
}GET /odps/{odp} mengembalikan satu ODP dengan bentuk yang sama.
Kaitan ONU↔ODP disimpan sebagai posisi (olt, slot, port, onu_id), lalu di-enrich
status live dari snapshot polling terakhir.
{
"data": [
{
"snmp_olt_id": 2, "slot": 2, "port": 3, "onu_id": 80,
"serial_number": "ZTEGCF0995D0", "interface": "gpon-onu_1/2/3:80",
"name": "#2310095708 Ika Kulon Studio", "online": true, "has_live": true,
"rx_power_dbm": -25.852, "rx_power_label": "-25.852 dBm",
"latitude": null, "longitude": null
}
],
"meta": { "odp_id": 26, "count": 6, "online": 5 }
}has_live: false⇒ ONU tak ada lagi di snapshot OLT (kaitan ODP-nya basi), bukan sekadar offline.latitude/longitudeberasal dari pin peta ONU (null bila ONU belum di-pin).
Satu request untuk seluruh peta: pin ONU pelanggan, pin ODP beserta ONU yang
tersambung (untuk menggambar garis ODP→ONU), daftar OLT untuk filter, dan titik
tengah default. Query param: olt_id (opsional).
{
"data": {
"pins": [
{ "id": 9, "olt_id": 1, "olt_name": "OLT-C320-PATI", "slot": 1, "port": 1,
"onu_id": 5, "interface": "gpon-onu_1/1/1:5", "serial_number": "ZTEG00000005",
"latitude": -6.7, "longitude": 111.0, "customer_name": "Bu Sri",
"address": null, "phone": null, "notes": null,
"rx_power_dbm": -21.5, "rx_power_label": "-21.50 dBm",
"online": true, "has_live": true }
],
"odps": [
{ "id": 26, "snmp_olt_id": 2, "olt_name": "OLT-C300-SEKARJALAK",
"name": "ODP BANGPE", "slot": 2, "port": 3,
"latitude": -6.61, "longitude": 111.06, "locked": true, "notes": null,
"onus": [ /* bentuk sama dengan /odps/{id}/onus */ ] }
],
"olts": [{ "id": 1, "name": "OLT-C320-PATI" }],
"default_center": { "lat": -6.6168, "lng": 111.0568, "zoom": 12 }
},
"meta": { "pins": 0, "odps": 22 }
}default_center dihitung dari rata-rata pin ONU dan pin ODP (fallback: Pati),
supaya peta tetap terbuka di area kerja meski ONU-nya belum di-pin.
Peta di aplikasi bersifat baca-saja: menambah/menggeser pin & CRUD ODP tetap lewat dashboard web.
Query params:
| Param | Default | Keterangan |
|---|---|---|
status |
active |
active | cleared | all |
severity |
semua | critical | major | minor | warning |
type |
semua | olt_unreachable,port_down,los,dying_gasp,onu_offline,high_rx_attenuation |
olt_id |
semua | Saring 1 OLT |
page |
1 |
Halaman |
per_page |
50 |
Maks 200 |
{
"data": [
{
"id": 12,
"olt_id": 1,
"olt_name": "OLT-C320-PATI",
"type": "onu_offline",
"type_label": "ONU offline",
"severity": "major",
"status": "active",
"scope": "onu",
"slot": 1,
"port": 1,
"onu_id": 5,
"serial_number": "ZTEGC1234567",
"message": "ONU offline (dying gasp)",
"first_seen_at": "2026-06-28T09:00:00+07:00",
"last_seen_at": "2026-06-28T10:14:00+07:00",
"cleared_at": null,
"target": {
"resource_type": "onu",
"olt_id": 1,
"slot": 1,
"port": 1,
"onu_id": 5,
"openable": true,
"reason": null
}
}
],
"meta": { "total": 3, "per_page": 50, "current_page": 1, "last_page": 1, "count": 3 }
}slot/port/onu_id de primer nivel son los del momento en que se registró la alarma.
No navegues con ellos: si la ONU fue reprovisionada, otra ONU puede ocupar hoy esa
posición y abrirías el cliente equivocado.
Usa target, que el servidor resuelve siguiendo serial_number en el inventario actual:
| Campo | Significado |
|---|---|
resource_type |
onu | port | olt — qué pantalla corresponde |
slot/port/onu_id |
Posición actual (puede diferir de la histórica) |
openable |
false ⇒ no abrir el recurso; como máximo el OLT |
reason |
null si todo limpio; si no: onu_moved (se siguió el serial), position_reused (la ocupa otra ONU), onu_not_found, incomplete_location, olt_unavailable |
openable no depende de la capability web supports_cli_onu_detail: la app móvil tiene
su propia pantalla de detalle de ONU (alimentada por esta API) y funciona para todas las
familias. Se resuelve sin SNMP/Telnet — solo con el snapshot cacheado.
const BASE = "https://nms.bmkv.net/api/v1";
async function login(email, password) {
const res = await fetch(`${BASE}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ email, password, device_name: "Web Billing" }),
});
if (!res.ok) throw new Error("Login gagal");
const { data } = await res.json();
return data.token; // simpan
}
async function getOfflineOnus(token) {
const res = await fetch(`${BASE}/onus?status=offline&per_page=100`, {
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
});
const json = await res.json();
return json.data; // array ONU offline
}// --- Model ---
data class LoginReq(val email: String, val password: String, val device_name: String)
data class LoginRes(val data: TokenData)
data class TokenData(val token: String, val token_type: String, val user: User)
data class Envelope<T>(val data: T, val meta: Meta?)
data class Onu(
val olt_name: String, val interface: String?, val serial_number: String?,
val customer_name: String?, val online: Boolean, val rx_power_dbm: Double?
)
data class Meta(val total: Int, val per_page: Int, val current_page: Int, val last_page: Int)
// --- Service ---
interface NmsApi {
@POST("auth/login")
suspend fun login(@Body body: LoginReq): LoginRes
@GET("onus")
suspend fun onus(
@Header("Authorization") bearer: String,
@Query("status") status: String? = null,
@Query("q") q: String? = null,
@Query("page") page: Int = 1,
@Query("per_page") perPage: Int = 50
): Envelope<List<Onu>>
@GET("summary")
suspend fun summary(@Header("Authorization") bearer: String): Envelope<Map<String, Any>>
}
// --- Pemakaian ---
val api = Retrofit.Builder()
.baseUrl("https://nms.bmkv.net/api/v1/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(NmsApi::class.java)
val token = api.login(LoginReq("admin@bmkv.net", "rahasia", "Android - Budi")).data.token
val bearer = "Bearer $token"
val offline = api.onus(bearer, status = "offline").data$client = new GuzzleHttp\Client(['base_uri' => 'https://nms.bmkv.net/api/v1/']);
$token = json_decode($client->post('auth/login', ['json' => [
'email' => 'admin@bmkv.net', 'password' => 'rahasia', 'device_name' => 'Billing',
]])->getBody(), true)['data']['token'];
$onus = json_decode($client->get('onus', [
'query' => ['status' => 'offline'],
'headers' => ['Authorization' => "Bearer {$token}", 'Accept' => 'application/json'],
])->getBody(), true)['data'];- Setelah deploy perubahan rute, rebuild cache rute di produksi:
php artisan route:cache && php artisan config:cache, lalu pastikan nginx meneruskan/api/*ke PHP-FPM (umumnya sudah, karena semua di-handle Laravel). - Migrasi tabel token:
php artisan migrate(membuatpersonal_access_tokens). - Token tak kedaluwarsa otomatis kecuali diatur. Cabut manual lewat
/auth/logoutatau hapus baris di tabelpersonal_access_tokens. - Memperbesar/mengubah rate limit: edit limiter
apidiapp/Providers/AppServiceProvider.php.
- Aksi tulis lanjutan: enable/disable ONU (set state).
- Webhook/push event alarm real-time.
- Filter rentang waktu & ekspor.
Bila butuh salah satu di atas, ajukan agar ditambahkan di v2.