← pulsakonter.com / Dokumentasi API
Draf Dokumen ini adalah rancangan kontrak — sistemnya belum dibangun. Tujuannya supaya bentuk API bisa dikoreksi dulu sebelum ada kode, karena mengubah kontrak setelah distributor berintegrasi jauh lebih mahal.
Dokumentasi API

API Distributor

Antarmuka host-to-host untuk melakukan pembelian produk digital secara otomatis dari sistem Anda sendiri. Ditujukan untuk mitra distributor yang sudah memiliki saldo di Pulsakonter.

Base URL

Produksi
https://api.pulsakonter.com/v1

Sandbox — belum tersedia, nama sudah dicadangkan
https://sandbox-api.pulsakonter.com/v1

Seluruh endpoint memakai metode POST dan bertukar data JSON — termasuk endpoint yang sifatnya hanya membaca, seperti cek saldo. Ini disengaja: tanda tangan permintaan menyertakan ringkasan isi body, sehingga aturannya jadi seragam dan tidak ada kasus khusus yang bisa salah diterapkan.

Prasyarat

Empat hal berikut harus terpenuhi sebelum permintaan pertama Anda diterima.

SyaratKeterangan
Role DistributorAkun Anda harus berrole Distributor. Harga yang berlaku di API adalah harga tier Distributor, bukan harga etalase.
SaldoOrder hanya bisa dibayar dengan saldo. Tidak ada metode pembayaran lain di API — isi saldo lebih dulu melalui halaman member.
PINAkun harus sudah memiliki PIN. PIN dikirim pada setiap permintaan order.
Whitelist IPPermintaan hanya diterima dari alamat IP yang Anda daftarkan di halaman API. IP di luar daftar ditolak sebelum tanda tangan sempat diperiksa.

Kredensial, whitelist IP, dan URL callback diatur sendiri lewat menu API di area member Anda. Mengubah salah satunya meminta PIN.

Kredensial

Anda memegang dua nilai dengan peran yang berbeda. Membedakan keduanya penting: satu boleh terlihat orang lain, satu lagi tidak boleh sama sekali.

API Key
Penanda · ikut terkirim

Menyatakan siapa yang memanggil. Dikirim apa adanya di header X-Client-Id pada setiap permintaan, jadi nilainya memang terlihat — perannya seperti username, bukan kata sandi. Aman bila tercatat di log Anda sendiri.

pk_live_7f3a9c2e81b4d650
Secret Key
Kunci tanda tangan · tidak pernah dikirim

Dipakai hanya untuk menghitung X-Signature. Nilainya tidak pernah ikut dalam permintaan — yang terkirim hanyalah hasil perhitungannya. Perannya seperti kata sandi: siapa pun yang memegangnya bisa berpura-pura menjadi Anda.

sk_live_9d81ff4c6a27e05b3ac1…

Inilah alasan tanda tangan lebih aman daripada sekadar mengirim kunci rahasia di header: kalaupun sebuah permintaan berhasil disadap, penyadap hanya mendapat API Key dan satu tanda tangan yang hanya berlaku untuk permintaan itu saja — bukan kunci yang bisa dipakai membuat permintaan baru.

Mendapatkan dan mengganti kredensial

Keduanya diterbitkan otomatis saat akses API akun Anda diaktifkan, dan bisa dilihat di menu API pada area member. Secret Key hanya ditampilkan penuh sesaat setelah dibuat; setelah itu tersimpan terenkripsi dan tidak dapat ditampilkan lagi — bila terlupa, generate ulang.

Generate ulang membuat Secret Key lama langsung tidak berlaku, sehingga integrasi yang masih memakainya akan gagal dengan rc 11. Siapkan penggantian di sisi Anda sebelum menekan tombolnya.

Autentikasi

Setiap permintaan membawa tiga header. Tidak ada token yang perlu disimpan atau diperbarui — tanda tangan dihitung ulang per permintaan.

HeaderIsi
X-Client-IdAPI Key Anda — lihat Kredensial.
X-Request-TimeWaktu permintaan dalam format ISO 8601, contoh 2026-08-05T09:14:22+07:00 atau 2026-08-05T02:14:22Z.
X-SignatureHMAC-SHA256 dalam huruf kecil heksadesimal, dihitung dengan Secret Key. Cara membentuknya di bawah.

Membentuk tanda tangan

  1. Susun body JSON, lalu buang seluruh spasi dan baris baru sehingga menjadi satu baris rapat.
  2. Hitung MD5 dari string body tersebut.
  3. Rangkai empat bagian dipisah titik dua: metode, path tanpa garis miring depan, hasil MD5, dan X-Request-Time.
  4. Hitung HMAC-SHA256 atas rangkaian itu menggunakan Secret Key Anda sebagai kunci.
// 1 — body dirapatkan
{"service_code":"ML10","reference_number":"INV-0001",...}

// 2 — md5 dari body di atas
ca06c494d590574127263feeed50bae5

// 3 — string yang ditandatangani
POST:v1/order:ca06c494d590574127263feeed50bae5:2026-08-05T09:14:22+07:00

// 4 — hasil akhir, masuk ke header X-Signature
hmac_sha256(secret, string_di_atas)  // hex, huruf kecil
Batas waktu

X-Request-Time ditolak bila selisihnya lebih dari 5 menit dari jam server kami. Pastikan jam server Anda tersinkron NTP — jam yang meleset adalah penyebab kegagalan integrasi yang paling sering terjadi dan paling membingungkan, karena tanda tangannya sendiri sudah benar.

Menjaga Secret Key

Secret Key hanya dipakai untuk menghitung tanda tangan dan tidak pernah dikirim dalam permintaan. Jangan menaruhnya di kode sisi klien, repositori publik, atau log. Bila bocor, generate ulang dari halaman API — tanda tangan lama langsung tidak berlaku.

Satu string, dipakai dua kali

Nilai X-Request-Time di header harus sama persis dengan yang Anda masukkan ke rangkaian tanda tangan. Hitung sekali, simpan di variabel, lalu pakai di kedua tempat. Membangkitkannya dua kali secara terpisah adalah penyebab rc 11 yang paling sulit dilacak, karena selisih satu detik saja sudah membuat tanda tangan berbeda.

Contoh kode

Fungsi penanda tangan beserta contoh pemanggilan endpoint order. Semua contoh menghasilkan tanda tangan yang identik untuk masukan yang sama.

// Node.js 18+ — tanpa dependensi eksternal
const crypto = require('crypto');

const BASE_URL   = 'https://api.pulsakonter.com';
const API_KEY    = process.env.PK_API_KEY;     // pk_live_…
const SECRET_KEY = process.env.PK_SECRET_KEY;  // sk_live_… jangan di-hardcode

function buatSignature(pathname, payload, requestTime) {
  const md5 = crypto.createHash('md5').update(payload).digest('hex');
  const raw = `POST:${pathname}:${md5}:${requestTime}`;
  return crypto.createHmac('sha256', SECRET_KEY).update(raw).digest('hex');
}

async function panggil(pathname, body) {
  // JSON.stringify sudah menghasilkan string rapat tanpa spasi/baris baru.
  // Payload yang ditandatangani HARUS string yang sama dengan yang dikirim.
  const payload     = JSON.stringify(body);
  const requestTime = new Date().toISOString();

  const res = await fetch(`${BASE_URL}/${pathname}`, {
    method:  'POST',
    headers: {
      'Content-Type':   'application/json',
      'X-Client-Id':    API_KEY,
      'X-Request-Time': requestTime,
      'X-Signature':    buatSignature(pathname, payload, requestTime),
    },
    body: payload,
  });
  return res.json();
}

// Contoh: buat order
panggil('v1/order', {
  service_code:     'ML10',
  reference_number: 'INV-0001',
  inputs:           { user_id: '12345678', zone_id: '2201' },
  price:            3000,
  qty:              1,
  pin:              process.env.PK_PIN,
}).then(console.log);
Jangan bangun ulang JSON-nya

Tandatangani string body yang sama persis dengan yang Anda kirimkan — jangan menyusun ulang JSON untuk keperluan tanda tangan. Urutan kunci, spasi, dan cara meng-escape karakter bisa berbeda antar pemanggilan, dan perbedaan satu karakter saja membuat MD5 — dan seluruh tanda tangannya — tidak cocok. Pada semua contoh di atas, satu variabel payload dipakai untuk keduanya.

Memverifikasi tanda tangan callback

Perhitungannya sama, hanya pathname-nya diganti path endpoint callback Anda sendiri.

// Node.js — verifikasi callback masuk
const diterima = req.headers['x-signature'];
const harusnya = buatSignature('webhook/pulsakonter', rawBody, req.headers['x-request-time']);

// Bandingkan dengan timingSafeEqual, bukan === , agar waktu perbandingan
// tidak membocorkan informasi tentang tanda tangan yang benar.
const cocok = diterima?.length === harusnya.length &&
  crypto.timingSafeEqual(Buffer.from(diterima), Buffer.from(harusnya));

if (!cocok) return res.status(401).end();

rawBody harus berupa body mentah yang diterima, bukan hasil parse lalu stringify ulang. Di Express, gunakan express.raw() atau simpan body mentahnya lewat opsi verify sebelum JSON di-parse.

Kode respons

Setiap respons membawa rc dan message. HTTP status selalu 200 selama permintaan berhasil diterima dan diautentikasi — hasil bisnisnya dibaca dari rc, bukan dari HTTP status.

rcArtiTindakan yang disarankan
00Sukses
01Transaksi sedang diprosesTunggu callback. Jangan kirim ulang order.
02Transaksi gagalSaldo sedang dikembalikan otomatis.
03Saldo sudah dikembalikanStatus akhir. Aman untuk order ulang.
10Client ID tidak dikenalPeriksa header X-Client-Id.
11Tanda tangan tidak validPeriksa perapatan body dan urutan rangkaian.
12Waktu permintaan kedaluwarsaSinkronkan jam server Anda.
13IP tidak diizinkanTambahkan IP di halaman API.
14PIN salahHentikan pengiriman ulang. Lihat catatan di bawah.
15Akses API nonaktifHubungi kami.
20Produk tidak ditemukanPerbarui katalog Anda.
21Produk sedang nonaktifCoba lagi nanti atau pakai produk lain.
22Harga tidak cocokAmbil ulang harga dari daftar produk.
30Saldo tidak cukupIsi saldo.
31Data akun tidak lengkapLengkapi inputs sesuai inputs_schema.
40Nomor referensi sudah dipakai untuk order berbedaGunakan nomor referensi baru.
99Kesalahan sistemCoba lagi beberapa saat kemudian.
PIN salah berulang

PIN yang salah dikirim berkali-kali akan mengunci akses API Anda selama 15 menit. Penguncian ini khusus API dan tidak memengaruhi PIN Anda di area member. Bila mendapat rc 14, hentikan pengiriman dan perbaiki konfigurasi — mengulang otomatis hanya memperpanjang penguncian.

Saldo

Mengembalikan sisa saldo akun Anda dalam Rupiah.

POST/v1/balance

Permintaan

{}

Body kosong, tetapi tetap harus dikirim sebagai {} dan ikut ditandatangani.

Respons

{
  "rc": "00",
  "message": "Sukses",
  "data": {
    "balance": 1250000,
    "currency": "IDR"
  }
}

Daftar produk

Mengembalikan katalog beserta harga tier Distributor Anda dan bentuk data akun yang dibutuhkan tiap produk.

POST/v1/products

Permintaan

FieldTipeKeterangan
updated_sinceopsionalstringISO 8601. Bila diisi, hanya produk yang berubah sejak waktu itu yang dikembalikan. Gunakan untuk sinkronisasi berkala tanpa menarik seluruh katalog.
category_codeopsionalstringBatasi ke satu kategori.

Respons

{
  "rc": "00",
  "message": "Sukses",
  "data": [
    {
      "service_code": "ML10",
      "service_name": "10 Diamonds",
      "category_code": "MLBB",
      "category_name": "Mobile Legends",
      "price": 3000,
      "status": "active",
      "updated_at": "2026-08-05T09:00:00+07:00",
      "inputs_schema": [
        { "key": "user_id", "label": "User ID", "type": "number", "required": true },
        { "key": "zone_id", "label": "Zone ID", "type": "number", "required": true }
      ]
    }
  ]
}

inputs_schema adalah kontrak untuk field inputs saat membuat order. Produk pulsa umumnya hanya punya satu entri; game biasanya dua. Simpan skema ini bersama katalog Anda agar validasi bisa dilakukan di sisi Anda sebelum order dikirim.

Buat order

POST/v1/order

Permintaan

FieldTipeKeterangan
service_codewajibstringKode produk dari daftar produk.
reference_numberwajibstringNomor referensi milik Anda, unik per akun. Maks. 64 karakter.
inputswajibobjectData akun tujuan, berkunci sesuai inputs_schema produk.
pricewajibnumberHarga satuan yang Anda yakini berlaku. Bila tidak cocok, order ditolak dengan rc 22.
pinwajibstringPIN 6 digit akun Anda.
qtyopsionalnumberJumlah pembelian. Default 1.
{
  "service_code": "ML10",
  "reference_number": "INV-0001",
  "inputs": {
    "user_id": "12345678",
    "zone_id": "2201"
  },
  "price": 3000,
  "qty": 1,
  "pin": "123456"
}

Respons

{
  "rc": "01",
  "message": "Transaksi sedang diproses",
  "data": {
    "order_id": "TRX-20260805-A1B2C",
    "reference_number": "INV-0001",
    "service_code": "ML10",
    "service_name": "10 Diamonds",
    "status": "PROCESSING",
    "price": 3000,
    "qty": 1,
    "total": 3000,
    "sn": "",
    "balance_after": 1247000,
    "created_at": "2026-08-05T09:14:22+07:00"
  }
}
Idempotensi

Mengirim ulang reference_number yang sama tidak membuat order kedua — Anda menerima kembali status order yang sudah ada. Ini berlaku juga ketika koneksi Anda terputus sebelum respons diterima: cukup kirim ulang permintaan yang sama persis, saldo tidak akan terpotong dua kali.

Bila reference_number yang sama dikirim dengan isi berbeda, permintaan ditolak dengan rc 40.

Saldo dipotong saat order diterima. Bila order berakhir gagal, saldo dikembalikan otomatis dan status menjadi REFUNDED — Anda tidak perlu mengajukan apa pun.

Cek status

Gunakan sebagai jaring pengaman bila callback tidak diterima. Bukan pengganti callback — jangan melakukan polling ketat.

POST/v1/order/status

Permintaan

Isi salah satu dari dua field berikut.

FieldTipeKeterangan
reference_numberstringNomor referensi Anda.
order_idstringNomor order dari sistem kami.
{ "reference_number": "INV-0001" }

Respons

Bentuknya sama persis dengan respons buat order, dengan status dan sn terkini.

Callback status transaksi

Dikirim ke URL callback transaksi Anda setiap kali sebuah order mencapai status akhir. Kami mengirim POST JSON dengan header tanda tangan yang sama seperti permintaan Anda ke kami — sehingga Anda bisa memverifikasinya dengan kode yang cara kerjanya identik.

{
  "event": "transaction.status",
  "data": {
    "order_id": "TRX-20260805-A1B2C",
    "reference_number": "INV-0001",
    "service_code": "ML10",
    "status": "SUCCESS",
    "rc": "00",
    "message": "Transaksi berhasil",
    "sn": "SN-8842190",
    "price": 3000,
    "qty": 1,
    "total": 3000,
    "balance_after": 1247000,
    "updated_at": "2026-08-05T09:15:04+07:00"
  }
}

Balas dengan HTTP 200 untuk menandakan callback sudah diterima. Respons selain 200, atau tidak ada respons dalam 10 detik, dianggap gagal dan akan diulang dengan jeda bertambah: 1, 5, 15, 60, lalu 300 detik. Setelah lima kali gagal, callback berhenti dan Anda perlu mengambil statusnya lewat cek status.

Idempotensi di sisi Anda

Callback yang sama bisa terkirim lebih dari sekali — misalnya bila respons 200 Anda hilang di jalan. Pastikan pemrosesan di sisi Anda aman diulang, dengan berpatokan pada reference_number.

Callback perubahan produk

Dikirim ke URL callback produk Anda ketika ada perubahan harga atau status produk. Perubahan dikumpulkan lebih dulu selama ±20 detik, lalu dikirim sebagai satu callback berisi array.

Kenapa dikumpulkan

Satu tindakan admin — misalnya mengubah margin sebuah kategori — dapat mengubah harga ratusan produk sekaligus. Bila tiap produk mengirim callback sendiri, endpoint Anda akan menerima ratusan permintaan dalam sekejap. Dengan pengumpulan, satu tindakan admin menghasilkan satu callback, dan Anda cukup memprosesnya dalam satu transaksi basis data.

{
  "event": "product.changed",
  "data": [
    {
      "service_code": "ML10",
      "price": 3100,
      "status": "active",
      "changed": ["price"],
      "updated_at": "2026-08-05T09:20:00+07:00"
    },
    {
      "service_code": "ML86",
      "price": 21000,
      "status": "inactive",
      "changed": ["status"],
      "updated_at": "2026-08-05T09:20:00+07:00"
    }
  ]
}

Field changed menyebutkan apa saja yang berubah, sehingga Anda bisa memperbarui seperlunya. Aturan percobaan ulang sama dengan callback transaksi.

Verifikasi tanda tangan callback

Callback dari kami membawa header X-Client-Id, X-Request-Time, dan X-Signature. Rangkaian yang ditandatangani memakai path callback Anda sendiri, tanpa garis miring depan.

// URL callback Anda: https://server-anda.com/webhook/pulsakonter
POST:webhook/pulsakonter:<md5(body dirapatkan)>:<X-Request-Time>
Selalu verifikasi

Tanpa verifikasi, siapa pun yang mengetahui URL callback Anda bisa mengirim notifikasi palsu — misalnya mengaku sebuah order sudah SUCCESS padahal belum. Bandingkan tanda tangan sebelum memproses isinya, dan tolak bila tidak cocok.

Status transaksi

PROCESSING SUCCESS atau FAILED REFUNDED
StatusrcArti
PROCESSING01Order diterima dan sedang diproses. Saldo sudah dipotong. Status belum final.
SUCCESS00Produk terkirim. sn berisi serial number bila produk memilikinya. Status akhir.
FAILED02Produk gagal terkirim. Pengembalian saldo sedang berjalan — status ini bersifat sementara.
REFUNDED03Saldo sudah kembali ke akun Anda. Status akhir.

Anda akan menerima callback pada SUCCESS dan REFUNDED. Bila sebuah order tertahan di PROCESSING lebih lama dari biasanya, itu berarti sedang menunggu penanganan manual — jangan mengirim order pengganti, karena order aslinya masih bisa berakhir sukses.

Aturan penting

Harga diperiksa saat order

Field price yang Anda kirim dibandingkan dengan harga tier Distributor yang berlaku saat itu. Bila berbeda, order ditolak — tidak diproses diam-diam dengan harga lain. Ini melindungi kedua pihak ketika katalog Anda tertinggal dari perubahan harga kami.

Satu nomor referensi, satu order

reference_number harus unik di akun Anda. Ini yang membuat pengiriman ulang aman saat jaringan bermasalah.

Kegagalan selalu dikembalikan

Tidak ada order gagal yang menahan saldo Anda. Pengembalian berjalan otomatis dan tercatat di mutasi saldo.

Batas laju

Maksimal 10 permintaan per detik per akun. Melebihi batas akan mendapat rc 99 sementara.