API Publik /api/v1
Dokumen ini untuk tim client headless commerce yang mengonsumsi SiDIVA-TS. Semua endpoint di sini adalah API server-to-server: satu API key per client, tanpa cookie, tanpa session browser.
Versi web dokumen ini tersedia di
/docspada server SiDIVA-TS yang sama (publik, tanpa login admin) dan dirender langsung dari berkas Markdown ini.
Endpoint dashboard lama (
/api/*) tetap ada untuk UI internal dan tidak boleh dipakai client. Kontrak publik hanya/api/v1/*.
1. Ringkasan
| Kemampuan | Endpoint |
|---|---|
| Profil + saldo dompul | GET /api/v1/account |
| Katalog produk (DB / live) | GET /api/v1/products |
| Kategori produk | GET /api/v1/products/categories |
| Cek nomor & kuota (XL/AXIS/Smartfren) | GET /api/v1/check |
| Promo NBO per nomor | GET /api/v1/promos |
| Riwayat transaksi | GET /api/v1/history |
| Status order | GET /api/v1/orders/{orderId} |
| Beli pulsa | POST /api/v1/purchases/pulsa |
| Beli paket | POST /api/v1/purchases/package |
| Topup saldo dompul | POST /api/v1/payments/topup |
Model integrasi
Client (headless commerce) → SiDIVA-TS /api/v1 → backend XL
X-API-Key sesi dealer tersimpan
Server SiDIVA-TS memegang satu sesi akun dealer XL (token disimpan di ~/.sidiva-ts/session.json). Client tidak perlu dan tidak boleh mengirim token XL. Login OTP dealer dilakukan sekali oleh operator lewat dashboard/CLI:
bun run src/cli.ts otp 628xxxxxxxxxx
bun run src/cli.ts verify 628xxxxxxxxxx 123456
2. Konfigurasi server
Isi .env (atau environment proses):
| Variabel | Wajib | Keterangan |
|---|---|---|
SIDIVA_API_KEYS | ya | Daftar API key dipisah koma, mis. key_toko_a,key_toko_b. Kosong → semua /api/v1 menjawab 503. |
SIDIVA_DEALER_PIN | ya* | PIN dealer untuk transaksi. Tidak pernah disimpan ke disk/log. |
SIDIVA_HOST | tidak | Default 127.0.0.1. Set 0.0.0.0 bila diakses lewat reverse proxy. |
SIDIVA_PORT | tidak | Default 7788. |
SIDIVA_IMEI | untuk login | IMEI perangkat yang dipakai saat login OTP. |
SIDIVA_DEALER_CGI / SIDIVA_DEALER_GPS | tidak | Metadata lokasi transaksi; default mengikuti aplikasi asli. |
SIDIVA_API_DB | tidak | Path SQLite idempotency. Default data/api.db. |
SIDIVA_PRODUCTS_DB | tidak | Path SQLite katalog. Default data/products.db. |
SIDIVA_ORDERS_DB | tidak | Path SQLite ledger order. Default data/orders.db. |
\* PIN juga bisa dikirim per request lewat header X-PIN (menimpa env). Untuk deployment multi-toko, cara paling aman adalah satu instance server per dealer dengan PIN di env, lalu client hanya memegang API key.
Rotasi API key: ubah SIDIVA_API_KEYS, restart server. Key lama langsung mati.
Keamanan
- Pasang HTTPS di reverse proxy. API key dan PIN tidak boleh lewat HTTP polos.
- Jangan pernah menaruh
X-API-KeyatauX-PINdi kode frontend/browser. - PIN hanya dipakai untuk memanggil upstream; server tidak menyimpannya.
- Tidak ada rate limit bawaan — batasi di reverse proxy (mis. 60 req/menit/key).
3. Autentikasi
Setiap request wajib:
X-API-Key: key_toko_a
Tanpa key / key salah:
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"ok": false,
"error": { "code": "unauthorized", "message": "X-API-Key tidak valid" }
}
Bila SIDIVA_API_KEYS belum diset di server:
{
"ok": false,
"error": { "code": "unconfigured", "message": "SIDIVA_API_KEYS belum diset di server" }
}
4. Format respons
Sukses
{
"ok": true,
"data": { },
"meta": { }
}
meta hanya ada pada endpoint list.
Gagal
{
"ok": false,
"error": {
"code": "invalid_request",
"message": "msisdn wajib diisi",
"detail": null
}
}
| HTTP | code | Kapan |
|---|---|---|
| 400 | invalid_request | Parameter/body tidak valid |
| 401 | unauthorized | API key tidak ada/salah |
| 404 | not_found | Order/endpoint tidak ditemukan |
| 409 | conflict | Idempotency-Key dipakai untuk payload berbeda / masih diproses |
| 502 | upstream_error | Backend XL menolak atau error; detail berisi respons upstream |
| 503 | unconfigured | API key atau PIN belum diset di server |
| 500 | internal_error | Bug server; laporkan ke operator |
Format nomor: semua msisdn di respons dinormalkan ke 62… (0878… → 62878…). Input boleh 08…, 62…, atau +62….
5. Idempotency (WAJIB untuk aksi berbiaya)
POST pembelian dan topup wajib menyertakan:
Idempotency-Key: 6f1c2f2e-8a6c-4f0e-9f1a-2b3c4d5e6f70
Gunakan UUID baru untuk setiap transaksi baru.
| Kondisi | Perilaku |
|---|---|
| Key baru | Request diproses, respons disimpan |
| Key sama + payload sama | Respons & status sama persis diputar ulang; upstream tidak dipanggil lagi |
| Key sama + payload berbeda | 409 conflict |
| Key sama saat request pertama belum selesai | 409 conflict (mencegah double charge) |
| Upstream menolak / server error | Klaim dilepas; client boleh retry dengan key yang sama |
Payload yang dibandingkan: field bisnis (msisdn, produk, nominal, callback). PIN tidak termasuk. Key disimpan di SQLite dan berlaku 7 hari.
Retry aman: kirim ulang request identik dengan key yang sama.
6. Endpoint
6.1 GET /api/v1/account
Profil dealer + saldo dompul. Tidak butuh parameter.
curl -s "$BASE/api/v1/account" -H "X-API-Key: $KEY"
{
"ok": true,
"data": {
"msisdn": "6287730525757",
"name": "Toko Demo",
"ownerName": "Budi",
"email": "demo@example.com",
"address": "Jl. Contoh No. 1",
"region": "SUB SOUTHERN SUMATRA",
"province": "SUMATERA BARAT",
"parentName": "PT Distributor Contoh",
"balance": {
"dompul": 34252,
"bj": [{ "denom": 5000, "unit": 0 }, { "denom": 10000, "unit": 0 }]
},
"wallet": null,
"point": null
}
}
balance.dompul= saldo dompul yang dipakai untuk transaksi.wallet/point= data upstream apa adanya, ataunullbila akun tidak punya layanan tersebut (bukan error).- Profil gagal diambil →
502 upstream_error; saldo gagal →balance.dompul = 0.
6.2 GET /api/v1/products
Katalog untuk storefront. Default dari database lokal (hasil sync operator).
Query
| Param | Default | Nilai |
|---|---|---|
brand | semua | xl, axis, smartfren (alias sf), pulsa, all |
category | semua | Nama kategori persis, mis. PACKET, VOICE |
q | – | Cari di nama atau kode produk |
sort | category | brand, category, price, name, updated |
order | asc | asc, desc |
page | 1 | ≥ 1 |
limit | 20 | 1–100 |
source | db | db atau live |
msisdn | sesi server | Hanya untuk source=live |
Contoh
curl -s "$BASE/api/v1/products?brand=xl&category=PACKET&sort=price&order=asc&page=1&limit=20" \
-H "X-API-Key: $KEY"
{
"ok": true,
"data": [
{
"code": "XL_PKT_10GB",
"name": "Xtra Combo 10GB",
"brand": "xl",
"category": "PACKET",
"family": "xl",
"quota": "10 GB",
"activePeriod": "30 hari",
"normalPrice": 55000,
"dompulPrice": 50000,
"price": 50000,
"updatedAt": "2026-10-31T03:00:00.000Z"
}
],
"meta": { "page": 1, "limit": 20, "total": 1, "totalPages": 1, "source": "db" }
}
price=harga_jualbila sudah diisi operator; bila masih 0 → memakaidompulPrice. Gunakanpriceuntuk checkout.dompulPrice= harga dealer dari XL.sort=pricemengurutkan berdasarkanpriceefektif (fallbackdompulPrice), jadi produk yang belum diisi harga jual tetap tersortir benar.
source=live
Mengambil katalog langsung dari XL/SF. Syarat:
brandwajib eksplisit (xl,axis,smartfren,pulsa).categorywajib untukxl/axis/smartfren; tidak perlu untukpulsa.msisdnwajib (atau sesi server terisi).
curl -s "$BASE/api/v1/products?source=live&brand=smartfren&category=DATA&msisdn=08891900300" \
-H "X-API-Key: $KEY"
Bentuk item sama. updatedAt = null, dan price memakai harga dompul live. Katalog live lebih lambat dan bisa terkena limit upstream — pakai DB untuk storefront, live hanya untuk verifikasi harga.
Sync katalog DB dilakukan operator (bukan client):
# butuh login admin dashboard
curl -s -X POST "$BASE/api/product-db/sync" \
-H "Content-Type: application/json" -b "sidiva_admin=..." \
-d '{"brand":"xl"}'
6.3 GET /api/v1/products/categories
Untuk membangun filter kategori di storefront.
| Param | Default | Nilai |
|---|---|---|
brand | semua | xl, axis, smartfren, pulsa, all |
source | db | db atau live |
msisdn | sesi server | Hanya source=live |
{
"ok": true,
"data": [
{ "category": "PACKET", "count": 42 },
{ "category": "VOICE", "count": 8 }
],
"meta": { "total": 2, "source": "db" }
}
source=live mengembalikan count: null karena daftar kategori upstream tidak menyertakan jumlah item. brand=pulsa tidak punya kategori live (400).
6.4 GET /api/v1/check
Cek nomor + paket/kuota aktif. Mendukung XL, AXIS, dan Smartfren.
| Param | Wajib | Keterangan |
|---|---|---|
msisdn | ya | 08…, 62…, atau +62… |
brand | tidak | auto (default), xl, axis, smartfren |
fresh | tidak | 1 = abaikan cache 5 menit (khusus paket XL) |
Deteksi auto berdasarkan prefix:
| Brand | Prefix |
|---|---|
| XL | 0817, 0818, 0819, 0859, 0877, 0878 |
| AXIS | 0831, 0832, 0833, 0838 |
| Smartfren | 0881–0889 |
Prefix tidak dikenal → 400 invalid_request; isi brand manual.
curl -s "$BASE/api/v1/check?msisdn=087812345678" -H "X-API-Key: $KEY"
{
"ok": true,
"data": {
"msisdn": "6287812345678",
"brand": "xl",
"subscriber": {
"status4G": { "status": "4G" },
"tenure": { "tenure": "1 Year 7 Month" },
"activeUntil": { "expDate": "2031-04-10" },
"activeSince": null,
"subscribedSince": null,
"dukcapil": { "dukcapil": "Registered" },
"spExpDate": { "SPExpDate": "2030-12-11" },
"volte": { "device": true, "area": true, "simcard": true },
"updatedAt": null
},
"packages": [
{
"name": "Bonus Paket Akrab",
"activeUntil": "2026-11-03T23:59:59",
"benefits": ["Bonus myRewards 7 GB"]
}
],
"lastUpdate": null
}
}
Catatan:
subscriber.*= payload mentah masing-masing cek. Bila satu cek gagal, field itu berisi objek error (mis.{ "error": 404, "detail": … }) dan cek lain tetap terisi.subscriber.tenurehanya XL/AXIS;subscriber.spExpDatehanya XL.subscriber.activeSince,subscribedSince, danupdatedAtterisi untuk Smartfren (masa aktif, masa berlangganan, tanggal update upstream); untuk XL/AXIS nilainyanullkarena upstream tidak menyediakannya di jalur ini.packagesseragam untuk XL/AXIS/Smartfren.- Bila paket XL gagal diambil (mis. limit),
packages=[]dan adapackagesError. - XL membatasi cek paket 5x per 3 jam per nomor. Server menyimpan hasil sukses 5 menit;
fresh=1memaksa ambil ulang (hati-hati limit). - Smartfren memakai satu panggilan upstream;
lastUpdateterisi bila tersedia.
6.5 GET /api/v1/promos
Promo NBO (next best offer) untuk satu nomor.
| Param | Wajib | Keterangan |
|---|---|---|
msisdn | tidak | Default nomor sesi server |
curl -s "$BASE/api/v1/promos?msisdn=087812345678" -H "X-API-Key: $KEY"
{
"ok": true,
"data": [
{
"brand": "xl",
"name": "FlexMax 7GB, 28hr, 37rb",
"category": "PACKET",
"normalPrice": 34500,
"dompulPrice": 34500,
"offerId": "E25UX3LemDsjO2QOU0LKWRUVME4O7m1ivs5djk8l...",
"cuanHot": 0,
"bonusCuanHot": null,
"viewDetails": true
}
],
"meta": { "msisdn": "6287812345678", "count": 1 }
}
Untuk membeli, sumber utama tetap productCode dari katalog (GET /products). offerId boleh diteruskan ke POST /api/v1/purchases/package bila upstream mensyaratkannya; kombinasi pembelian langsung dari NBO belum diverifikasi end-to-end, jadi jangan mengandalkan NBO sebagai satu-satunya sumber checkout. offerId bersifat sensitif terhadap nomor — jangan disimpan lintas nomor.
6.6 GET /api/v1/history
Riwayat transaksi dari XL.
| Param | Default | Keterangan |
|---|---|---|
startDate | 29 hari lalu | YYYY-MM-DD atau DD-MM-YYYY |
endDate | hari ini | YYYY-MM-DD atau DD-MM-YYYY |
page | 1 | ≥ 1 |
limit | 20 | 1–100 |
Rentang maksimal 30 hari (batasan upstream). Lebih dari itu → 400.
curl -s "$BASE/api/v1/history?startDate=2026-10-01&endDate=2026-10-30&limit=20" \
-H "X-API-Key: $KEY"
{
"ok": true,
"data": [
{
"transactionId": "TRX-123",
"msisdn": "6287812345678",
"product": "Pulsa 10rb",
"amount": 10000,
"status": "Sukses",
"result": "success",
"date": "2026-10-30T03:00:00.000Z"
}
],
"meta": {
"page": 1,
"limit": 20,
"total": 1,
"totalPages": 1,
"startDate": "2026-10-01",
"endDate": "2026-10-30"
}
}
result=success|failed|pending(normalisasi dari teks status XL).- Data diurutkan terbaru dulu, sehingga
limit=5pada rentang default memberi 5 transaksi terakhir. - Pagination dilakukan setelah data rentang diambil; upstream tidak punya parameter page untuk endpoint ini.
- Untuk transaksi yang baru dibuat, lebih akurat memakai
GET /api/v1/orders/{orderId}(pencocokan 1:1).
6.7 GET /api/v1/orders/{orderId}
Status order lokal. orderId didapat dari respons pembelian.
| Param | Keterangan |
|---|---|
refresh | 1 = cocokkan ulang dengan history XL (hanya bila status masih pending) |
curl -s "$BASE/api/v1/orders/2f0c...?refresh=1" -H "X-API-Key: $KEY"
{
"ok": true,
"data": {
"orderId": "2f0c...",
"kind": "package",
"status": "success",
"msisdn": "6287812345678",
"productCode": "XL_PKT_10GB",
"denom": null,
"amount": 50000,
"transactionId": "TRX-123",
"requestTransactionId": null,
"createdAt": "2026-10-30T03:00:00.000Z",
"matchedBy": "product",
"matchedAt": "2026-10-30T03:00:20.000Z",
"notifiedAt": null
}
}
status:pending|success|failed|unknown.transactionId= id transaksi XL setelah cocok; untuk pulsa,requestTransactionId= UUID yang dikirim saat order.matchedBy:transaction_id,amount,denom,product, ataumsisdn+time.- Tanpa
refresh=1, endpoint hanya membaca ledger lokal (cepat). - Pencocokan memakai history, bukan repost order — tidak ada risiko double charge.
6.8 POST /api/v1/purchases/pulsa
Beli pulsa ke nomor tujuan.
Header
X-API-Key: key_toko_a
Idempotency-Key: 6f1c2f2e-8a6c-4f0e-9f1a-2b3c4d5e6f70
X-PIN: 123456 # opsional; menimpa SIDIVA_DEALER_PIN
Content-Type: application/json
Body
| Field | Wajib | Keterangan |
|---|---|---|
msisdn | ya | Nomor tujuan |
denom | ya | Nominal pulsa, angka > 0 (mis. 10000) |
callbackUrl | tidak | URL http(s) untuk notifikasi final |
curl -s -X POST "$BASE/api/v1/purchases/pulsa" \
-H "X-API-Key: $KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"msisdn":"087812345678","denom":"10000"}'
{
"ok": true,
"data": {
"orderId": "2f0c...",
"kind": "w2p",
"status": "pending",
"msisdn": "6287812345678",
"denom": "10000",
"transactionId": "d4b2...",
"statusUrl": "/api/v1/orders/2f0c..."
}
}
HTTP 201. Order diterima upstream, belum tentu sukses — pantau statusUrl atau callback.
Upstream menolak → 502 upstream_error dengan detail respons XL. Klaim idempotency dilepas, jadi perbaiki penyebab lalu retry dengan key yang sama.
6.9 POST /api/v1/purchases/package
Beli paket/data. Field mengikuti katalog (GET /products) atau NBO (GET /promos).
Body
| Field | Wajib | Keterangan |
|---|---|---|
msisdn | ya | Nomor tujuan |
productCode | ya | code dari katalog/live |
amount | ya | Harga yang dikirim ke XL, angka > 0 |
offerId | tidak | Dari NBO (/promos) |
originalProduct | tidak | Bila upstream meminta produk asal |
paymentMethod | tidak | Default SMARTWALLET |
callbackUrl | tidak | URL http(s) notifikasi final |
curl -s -X POST "$BASE/api/v1/purchases/package" \
-H "X-API-Key: $KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"msisdn":"087812345678","productCode":"XL_PKT_10GB","amount":50000}'
{
"ok": true,
"data": {
"orderId": "9a1e...",
"kind": "package",
"status": "pending",
"msisdn": "6287812345678",
"productCode": "XL_PKT_10GB",
"amount": 50000,
"statusUrl": "/api/v1/orders/9a1e..."
}
}
HTTP 201. amount adalah otoritas harga yang dikirim ke XL; jangan mengarang harga — ambil dari price katalog atau dompulPrice NBO.
6.10 POST /api/v1/payments/topup
Inquiry topup saldo dompul. Ini bukan konfirmasi pembayaran: respons berisi instruksi pembayaran dari XL (mis. virtual account). Saldo masuk setelah pembayaran diselesaikan sesuai instruksi.
Body
| Field | Wajib | Keterangan |
|---|---|---|
nominal | ya | Nominal topup, angka > 0 |
bank | tidak | Default TFBANK |
curl -s -X POST "$BASE/api/v1/payments/topup" \
-H "X-API-Key: $KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"nominal":100000,"bank":"TFBANK"}'
{
"ok": true,
"data": {
"status": "pending",
"nominal": 100000,
"bank": "TFBANK",
"inquiry": {
"virtualAccount": "8808123456",
"bank": "TFBANK",
"totalAmount": 100000
}
}
}
Bentuk inquiry mengikuti respons upstream dan bisa berbeda per bank. Idempotency berlaku agar klik ganda tidak membuat dua inquiry.
7. Callback hasil transaksi
Bila callbackUrl dikirim pada pembelian, server mem-poll history XL di latar (interval 3 detik, maksimal 15 menit) dan sekali mengirim POST ke URL client saat hasil final (success/failed) ditemukan.
POST https://toko.example.com/webhooks/sidiva
Content-Type: application/json
{
"orderId": "2f0c...",
"status": "success",
"transactionId": "TRX-123",
"matchedBy": "transaction_id",
"attempts": 3,
"order": {
"msisdn": "6287812345678",
"kind": "w2p",
"productCode": null,
"denom": "10000",
"amount": 10000,
"transactionId": "d4b2...",
"createdAt": "2026-10-30T03:00:00.000Z"
},
"data": { "date": "2026-10-30 10:00:00.000", "product": "Pulsa 10rb", "status": "Sukses", "amount": "10000" }
}
Aturan untuk client:
- Balas
2xxcepat; pemrosesan berat taruh di antrean. - Callback hanya dikirim sekali, tidak ada retry otomatis. Tetap sediakan polling
GET /orders/{orderId}?refresh=1sebagai jaring pengaman. - Verifikasi
orderIdada di sistem Anda sebelum memproses. statusfinal bisasuccessataufailed. Timeout (15 menit) tidak mengirim callback; order tetappendingdan bisa dicek ulang.callbackUrlharus URLhttp(s)yang valid; selain itu400.
8. Alur integrasi yang disarankan
Checkout paket
1. GET /api/v1/products?brand=xl&category=PACKET&sort=price
2. GET /api/v1/check?msisdn={nomor} → tampilkan kuota berjalan
3. (opsional) GET /api/v1/promos?msisdn= → upsell NBO
4. POST /api/v1/purchases/package
Idempotency-Key: {uuid}
body: {msisdn, productCode, amount, callbackUrl}
5. Pantau status:
GET /api/v1/orders/{orderId}?refresh=1 (polling)
atau callback POST ke callbackUrl
Checkout pulsa
1. GET /api/v1/products?brand=pulsa
2. POST /api/v1/purchases/pulsa
Idempotency-Key: {uuid}
body: {msisdn, denom, callbackUrl}
3. GET /api/v1/orders/{orderId}?refresh=1
Topup saldo dompul
1. GET /api/v1/account → cek saldo
2. POST /api/v1/payments/topup → dapat instruksi pembayaran
3. Selesaikan pembayaran sesuai instruksi
Riwayat
GET /api/v1/history?startDate=2026-10-01&endDate=2026-10-30&page=1&limit=50
9. Batasan & catatan operasional
- Satu sesi dealer per instance. Semua client pada instance yang sama memakai akun XL yang sama.
- History maksimal 30 hari per request. Untuk laporan lebih panjang, ambil per 30 hari.
- Cek paket XL dibatasi 5x/3 jam per nomor. Server meng-cache hasil sukses 5 menit; jangan panggil
fresh=1berulang. topuphanya inquiry. Tidak ada endpoint konfirmasi pembayaran otomatis; pembayaran diselesaikan di channel bank/VA.wallet/pointbisanulltergantung tipe akun dealer.- Tidak ada rate limit bawaan. Pasang limit di reverse proxy.
- Angka uang selalu integer rupiah di respons publik (hasil normalisasi).
- Idempotency disimpan 7 hari. Setelah itu key boleh dipakai ulang.
- Katalog DB perlu di-sync operator secara berkala; client tidak bisa memicu sync lewat API key.
- Endpoint
/api/v1tidak memakai cookie admin, sehingga aman dipanggil server-to-server tanpa state browser.
10. Contoh cepat (curl)
export BASE="http://127.0.0.1:7788"
export KEY="key_toko_a"
curl -s "$BASE/api/v1/account" -H "X-API-Key: $KEY"
curl -s "$BASE/api/v1/products?brand=xl&category=PACKET&sort=price&order=asc" \
-H "X-API-Key: $KEY"
curl -s "$BASE/api/v1/check?msisdn=087812345678" -H "X-API-Key: $KEY"
curl -s "$BASE/api/v1/promos?msisdn=087812345678" -H "X-API-Key: $KEY"
curl -s "$BASE/api/v1/history?startDate=2026-10-01&endDate=2026-10-30" \
-H "X-API-Key: $KEY"
curl -s -X POST "$BASE/api/v1/purchases/pulsa" \
-H "X-API-Key: $KEY" \
-H "Idempotency-Key: 11111111-1111-4111-8111-111111111111" \
-H "Content-Type: application/json" \
-d '{"msisdn":"087812345678","denom":"10000"}'