SiDIVA-TS Dokumentasi API
Dashboard

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 /docs pada 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

KemampuanEndpoint
Profil + saldo dompulGET /api/v1/account
Katalog produk (DB / live)GET /api/v1/products
Kategori produkGET /api/v1/products/categories
Cek nomor & kuota (XL/AXIS/Smartfren)GET /api/v1/check
Promo NBO per nomorGET /api/v1/promos
Riwayat transaksiGET /api/v1/history
Status orderGET /api/v1/orders/{orderId}
Beli pulsaPOST /api/v1/purchases/pulsa
Beli paketPOST /api/v1/purchases/package
Topup saldo dompulPOST /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):

VariabelWajibKeterangan
SIDIVA_API_KEYSyaDaftar API key dipisah koma, mis. key_toko_a,key_toko_b. Kosong → semua /api/v1 menjawab 503.
SIDIVA_DEALER_PINya*PIN dealer untuk transaksi. Tidak pernah disimpan ke disk/log.
SIDIVA_HOSTtidakDefault 127.0.0.1. Set 0.0.0.0 bila diakses lewat reverse proxy.
SIDIVA_PORTtidakDefault 7788.
SIDIVA_IMEIuntuk loginIMEI perangkat yang dipakai saat login OTP.
SIDIVA_DEALER_CGI / SIDIVA_DEALER_GPStidakMetadata lokasi transaksi; default mengikuti aplikasi asli.
SIDIVA_API_DBtidakPath SQLite idempotency. Default data/api.db.
SIDIVA_PRODUCTS_DBtidakPath SQLite katalog. Default data/products.db.
SIDIVA_ORDERS_DBtidakPath 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-Key atau X-PIN di 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
  }
}
HTTPcodeKapan
400invalid_requestParameter/body tidak valid
401unauthorizedAPI key tidak ada/salah
404not_foundOrder/endpoint tidak ditemukan
409conflictIdempotency-Key dipakai untuk payload berbeda / masih diproses
502upstream_errorBackend XL menolak atau error; detail berisi respons upstream
503unconfiguredAPI key atau PIN belum diset di server
500internal_errorBug 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.

KondisiPerilaku
Key baruRequest diproses, respons disimpan
Key sama + payload samaRespons & status sama persis diputar ulang; upstream tidak dipanggil lagi
Key sama + payload berbeda409 conflict
Key sama saat request pertama belum selesai409 conflict (mencegah double charge)
Upstream menolak / server errorKlaim 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, atau null bila 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

ParamDefaultNilai
brandsemuaxl, axis, smartfren (alias sf), pulsa, all
categorysemuaNama kategori persis, mis. PACKET, VOICE
q–Cari di nama atau kode produk
sortcategorybrand, category, price, name, updated
orderascasc, desc
page1≥ 1
limit201–100
sourcedbdb atau live
msisdnsesi serverHanya 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_jual bila sudah diisi operator; bila masih 0 → memakai dompulPrice. Gunakan price untuk checkout.
  • dompulPrice = harga dealer dari XL.
  • sort=price mengurutkan berdasarkan price efektif (fallback dompulPrice), jadi produk yang belum diisi harga jual tetap tersortir benar.

source=live

Mengambil katalog langsung dari XL/SF. Syarat:

  • brand wajib eksplisit (xl, axis, smartfren, pulsa).
  • category wajib untuk xl/axis/smartfren; tidak perlu untuk pulsa.
  • msisdn wajib (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.

ParamDefaultNilai
brandsemuaxl, axis, smartfren, pulsa, all
sourcedbdb atau live
msisdnsesi serverHanya 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.

ParamWajibKeterangan
msisdnya08…, 62…, atau +62…
brandtidakauto (default), xl, axis, smartfren
freshtidak1 = abaikan cache 5 menit (khusus paket XL)

Deteksi auto berdasarkan prefix:

BrandPrefix
XL0817, 0818, 0819, 0859, 0877, 0878
AXIS0831, 0832, 0833, 0838
Smartfren0881–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.tenure hanya XL/AXIS; subscriber.spExpDate hanya XL.
  • subscriber.activeSince, subscribedSince, dan updatedAt terisi untuk Smartfren (masa aktif, masa berlangganan, tanggal update upstream); untuk XL/AXIS nilainya null karena upstream tidak menyediakannya di jalur ini.
  • packages seragam untuk XL/AXIS/Smartfren.
  • Bila paket XL gagal diambil (mis. limit), packages = [] dan ada packagesError.
  • XL membatasi cek paket 5x per 3 jam per nomor. Server menyimpan hasil sukses 5 menit; fresh=1 memaksa ambil ulang (hati-hati limit).
  • Smartfren memakai satu panggilan upstream; lastUpdate terisi bila tersedia.

6.5 GET /api/v1/promos

Promo NBO (next best offer) untuk satu nomor.

ParamWajibKeterangan
msisdntidakDefault 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.

ParamDefaultKeterangan
startDate29 hari laluYYYY-MM-DD atau DD-MM-YYYY
endDatehari iniYYYY-MM-DD atau DD-MM-YYYY
page1≥ 1
limit201–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=5 pada 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.

ParamKeterangan
refresh1 = 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, atau msisdn+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

FieldWajibKeterangan
msisdnyaNomor tujuan
denomyaNominal pulsa, angka > 0 (mis. 10000)
callbackUrltidakURL 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

FieldWajibKeterangan
msisdnyaNomor tujuan
productCodeyacode dari katalog/live
amountyaHarga yang dikirim ke XL, angka > 0
offerIdtidakDari NBO (/promos)
originalProducttidakBila upstream meminta produk asal
paymentMethodtidakDefault SMARTWALLET
callbackUrltidakURL 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

FieldWajibKeterangan
nominalyaNominal topup, angka > 0
banktidakDefault 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 2xx cepat; pemrosesan berat taruh di antrean.
  • Callback hanya dikirim sekali, tidak ada retry otomatis. Tetap sediakan polling GET /orders/{orderId}?refresh=1 sebagai jaring pengaman.
  • Verifikasi orderId ada di sistem Anda sebelum memproses.
  • status final bisa success atau failed. Timeout (15 menit) tidak mengirim callback; order tetap pending dan bisa dicek ulang.
  • callbackUrl harus URL http(s) yang valid; selain itu 400.

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=1 berulang.
  • topup hanya inquiry. Tidak ada endpoint konfirmasi pembayaran otomatis; pembayaran diselesaikan di channel bank/VA.
  • wallet/point bisa null tergantung 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/v1 tidak 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"}'