Ringkasan
Semua endpoint merchant memakai path berawalan /v1, mengirim dan
menerima JSON, dan dibungkus dalam satu bentuk respons yang konsisten (lihat Format
Respons). Kecuali POST /v1/auth, setiap permintaan wajib menyertakan header
Authorization: Bearer <token>.
Alur dasar
- Tukar
api_key+secret_keyAnda menjadi token lewatPOST /v1/auth. - (Khusus produk pascabayar) Cek tagihan lewat
POST /v1/inquiryuntuk tahu nominal sebenarnya. - Buat transaksi lewat
POST /v1/transactions— dengan atau tanpacallback_url. - Pantau hasilnya: lewat respons langsung (mode sinkron), polling
GET /v1/transactions/{id}, atau menerima webhook (mode asinkron).
Autentikasi
Tukarkan kredensial Anda dengan token bearer yang dipakai di semua permintaan berikutnya. Token berlaku singkat — desain ini sengaja agar token yang bocor cepat kedaluwarsa dengan sendirinya.
| Field | Tipe | Keterangan |
|---|---|---|
| api_key | string | Diberikan saat akun merchant Anda dibuat.wajib |
| secret_key | string | Simpan hanya di server Anda, jangan pernah di sisi client.wajib |
curl -X POST https://api.ppob-anda.co.id/v1/auth \ -H "Content-Type: application/json" \ -d '{ "api_key": "mck_live_xxxxxxxxxxxxxxxx", "secret_key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }'Contoh respons — 200 OK
{
"success": true,
"data": {
"token": "7f3a...c91e",
"expires_in": 30,
"expires_at": "2026-08-19T04:12:03Z"
},
"error": null
}
api_key/secret_key Anda-lah yang
disimpan jangka panjang, bukan token-nya.Gunakan token pada header berikut di setiap permintaan lain:
Authorization: Bearer 7f3a...c91e
api_key atau secret_key yang salah keduanya menjawab dengan
pesan identik — ini disengaja, supaya pihak luar yang mencoba menebak tidak bisa membedakan mana yang
benar dan mana yang salah.
Format Respons
Setiap respons — berhasil maupun gagal — memakai bentuk yang sama persis. Anda tidak
perlu menebak bentuk respons per endpoint; cukup periksa success.
{
"success": boolean,
"data": object | null,
"error": { "code": string, "message": string } | null
}
Ketiga field selalu ada, termasuk yang bernilai null — data dan
error tidak pernah keduanya terisi sekaligus. Lihat daftar lengkap kode
error untuk arti tiap error.code.
Tipe Produk
Ada dua tipe produk, dan keduanya menempuh jalur berbeda — ini menentukan apakah Anda
perlu memanggil /v1/inquiry sebelum membeli.
Prabayar (prepaid)
Pulsa, paket data, token listrik. Harga tetap dan sudah diketahui di muka — tidak
ada tagihan untuk dicek. Langsung panggil POST /v1/transactions; memanggil
/v1/inquiry untuk produk ini akan ditolak dengan INQUIRY_NOT_SUPPORTED.
Pascabayar (postpaid)
Tagihan listrik, PDAM, dan sejenisnya. Nominal sebenarnya baru diketahui setelah dicek ke biller — selalu inquiry dulu untuk tahu jumlah yang akan ditagihkan sebelum membayar.
/v1/inquiry adalah estimasi saat itu juga — tagihan bisa
berubah di antara waktu Anda cek dan waktu Anda bayar. Field price_is_estimate selalu
true pada respons inquiry sebagai pengingat ini.
Daftar Produk
Katalog produk yang tersedia untuk akun Anda — hanya produk yang sudah di-entitle ke merchant Anda yang muncul di sini.
{
"success": true,
"data": {
"products": [
{
"product_code": "V100AXS3a",
"name": "AXIS 100K",
"product_type": "prepaid",
"price_type": "fixed",
"sell_price": 10000,
"is_active": true
},
{
"product_code": "4156",
"name": "PDAM ATB BATAM",
"product_type": "postpaid",
"price_type": "by_bill",
"sell_price": 0,
"is_active": true
}
]
},
"error": null
}
price_type: "by_bill" berarti sell_price di sini bukan harga
final — cek lewat /v1/inquiry dulu. Untuk produk "fixed",
sell_price sudah final.
Cek Tagihan
Khusus produk pascabayar. Mengembalikan nominal yang akan ditagihkan beserta informasi pelanggan dari biller — tanpa membuat transaksi apa pun.
| Field | Tipe | Keterangan |
|---|---|---|
| product | string | Kode produk dari /v1/products.wajib |
| billNumber | string | Nomor pelanggan/meter/tagihan.wajib |
{
"success": true,
"data": {
"nominal": 254050,
"customer_info": "NAMA: BUDI SANTOSO|TARIF/DAYA: R1/900 VA|...",
"notice": "Bayar sebelum tanggal 20 untuk menghindari denda",
"extra_info": "",
"price_is_estimate": true
},
"error": null
}
nominal sudah termasuk biaya admin dan margin Anda — inilah jumlah yang akan
ditagihkan ke pelanggan Anda, bukan cuma nilai tagihan mentahnya.
customer_info/notice/extra_info adalah teks bebas dari biller
(detail rekening, pengumuman) — tampilkan apa adanya ke pelanggan Anda bila relevan.
Buat Transaksi
Endpoint inti — membeli produk prabayar atau membayar tagihan pascabayar. Perilakunya
bercabang dua tergantung apakah Anda mengirim callback_url.
| Header | Keterangan |
|---|---|
| Idempotency-Key | String unik per percobaan transaksi. Mengirim ulang key yang sama mengembalikan transaksi yang sama, bukan membuat transaksi baru — lihat Idempotency.wajib |
| Field | Tipe | Keterangan |
|---|---|---|
| product_code | string | Kode produk.wajib |
| bill_number | string | Nomor tujuan (HP, meter, pelanggan).wajib |
| merchant_reference | string | ID transaksi di sistem Anda sendiri, dikembalikan apa adanya untuk pencocokan. |
| callback_url | string | Lihat dua mode di bawah. |
Mode sinkron — tanpa callback_url
Kosongkan callback_url dan permintaan menunggu sampai transaksi benar-benar
selesai (sukses/gagal), lalu menjawab 200 OK dengan hasil akhirnya langsung. Cocok bila
sistem Anda lebih sederhana ditulis sebagai satu permintaan-satu jawaban, dan Anda tidak keberatan
permintaan berlangsung beberapa detik.
Mode asinkron — dengan callback_url
Permintaan langsung dijawab 202 Accepted dengan status
pending, tanpa menunggu biller. Hasil akhirnya dikirim ke callback_url Anda
lewat webhook begitu selesai. Cocok untuk volume tinggi atau bila permintaan HTTP
Anda tidak boleh menunggu lama.
curl -X POST https://api.ppob-anda.co.id/v1/transactions \ -H "Authorization: Bearer 7f3a...c91e" \ -H "Idempotency-Key: order-88213" \ -H "Content-Type: application/json" \ -d '{ "product_code": "4156", "bill_number": "119008812", "merchant_reference": "order-88213", "callback_url": "https://toko-anda.com/webhooks/ppob" }'Contoh respons — 202 Accepted (mode asinkron)
{
"success": true,
"data": {
"transaction_id": "c2f7c9d1-ee5d-46ef-80c6-0fda54d4e63f",
"merchant_reference": "order-88213",
"status": "pending",
"product_id": "5a65954f-e136-4736-b5f2-2bd3a7d5681c",
"bill_number": "119008812",
"nominal": null,
"serial_number": "",
"customer_info": "",
"notice": "",
"extra_info": "",
"failure_reason": null,
"submitted_at": "2026-08-19T04:12:05Z"
},
"error": null
}
nominal bernilai null selama status masih pending
— baru terisi begitu transaksi selesai. Bentuk respons ini sama persis dengan GET
/v1/transactions/{id} dan payload webhook, supaya kode yang membaca hasilnya bisa dipakai ulang di
ketiga tempat.
Detail Transaksi
Ambil status transaksi kapan saja setelah dibuat — dipakai untuk polling pada mode sinkron, atau mengonfirmasi ulang isi webhook yang Anda terima.
{id} adalah transaction_id yang dikembalikan saat transaksi
dibuat. Bentuk respons identik dengan Buat Transaksi — lihat contoh di sana.
404 TRANSACTION_NOT_FOUND yang identik, supaya ID transaksi tidak bisa dipakai
menebak-nebak data merchant lain.Saldo
{
"success": true,
"data": {
"available_balance": 480299,
"held_balance": 25409
},
"error": null
}
held_balance adalah dana yang sedang ditahan untuk transaksi
pending/unknown yang belum final — bukan dana hilang, hanya belum bisa dipakai
sampai transaksinya selesai. Saldo yang bisa dipakai untuk transaksi baru adalah
available_balance.
Mutasi Saldo
| Field | Tipe | Keterangan |
|---|---|---|
| limit | int | Default 20, maksimum 100. |
| offset | int | Default 0. |
{
"success": true,
"data": {
"entries": [
{
"transaction_id": "c2f7c9d1-ee5d-46ef-80c6-0fda54d4e63f",
"entry_type": "capture",
"amount": 25409,
"available_balance_after": 480299,
"held_balance_after": 0,
"created_at": "2026-08-18T05:48:48Z"
}
],
"limit": 20,
"offset": 0
},
"error": null
}
Webhook
Bila Anda mengirim callback_url saat membuat transaksi, kami mengirim
satu notifikasi POST ke URL tersebut begitu transaksi mencapai status final.
| X-Callback-Event-Id | ID unik pengiriman ini, untuk mendeteksi duplikat. |
| X-Callback-Signature | Tanda tangan HMAC-SHA256 dari body mentah — lihat verifikasi di bawah. |
{
"transaction_id": "c2f7c9d1-ee5d-46ef-80c6-0fda54d4e63f",
"merchant_reference": "order-88213",
"status": "success",
"nominal": 254050,
"serial_number": "a2d6adc50f9739d6cc24d019c8d08a43",
"customer_info": "NAMA: BUDI SANTOSO|...",
"failure_reason": null,
"updated_at": "2026-08-19T04:12:41Z"
}
Percobaan ulang
Jika endpoint Anda tidak membalas dengan status 2xx (termasuk timeout), kami
mencoba lagi hingga 5 kali dengan jeda sekitar 30 detik antar percobaan.
Balas 200 OK secepatnya setelah menerima payload — proses lebih lanjut boleh dilakukan asinkron
di sisi Anda, jangan buat kami menunggu.
GET /v1/transactions/{id} secara
berkala untuk transaksi yang Anda buat secara asinkron.Idempotency
Header Idempotency-Key pada POST /v1/transactions membuat
pengiriman ulang aman.
Bila koneksi Anda putus setelah mengirim permintaan tapi sebelum menerima jawaban, Anda tidak
tahu apakah transaksinya benar-benar terbuat. Kirim ulang permintaan yang persis sama
dengan Idempotency-Key yang sama, dan Anda akan menerima transaksi yang sudah ada — bukan
transaksi baru yang dobel. Gunakan satu key unik per percobaan pembelian (misalnya ID order Anda sendiri),
dan jangan pernah memakai ulang key yang sama untuk transaksi yang benar-benar berbeda.
Kode Error
Semua error menjawab dengan error.code yang stabil untuk logika program,
dan error.message berbahasa Indonesia untuk ditampilkan.
| Kode | HTTP | Pesan |
|---|---|---|
| INVALID_REQUEST | 400 | Format permintaan atau field wajib tidak valid. |
| UNAUTHORIZED | 401 | Kredensial atau token tidak valid/kedaluwarsa. |
| NOT_FOUND | 404 | Endpoint tidak ditemukan. |
| METHOD_NOT_ALLOWED | 405 | Metode HTTP tidak didukung untuk endpoint ini. |
| TRANSACTION_NOT_FOUND | 404 | Transaksi tidak ditemukan (atau bukan milik Anda). |
| INSUFFICIENT_BALANCE | 402 | Saldo tidak mencukupi untuk transaksi ini. |
| PRODUCT_NOT_ENTITLED | 403 | Produk tidak tersedia untuk akun Anda. |
| INQUIRY_NOT_SUPPORTED | 400 | Produk prabayar tidak memiliki tagihan untuk dicek. |
| CUSTOMER_NOT_FOUND | 400 | Nomor pelanggan tidak ditemukan pada biller. |
| BILLER_REJECTED | 502 | Biller menolak permintaan untuk produk ini. |
| BILLER_ERROR | 502 | Gagal memproses ke biller — coba lagi beberapa saat. |
| INTERNAL_ERROR | 500 | Kesalahan di sisi kami — hubungi dukungan bila berulang. |
PRODUCT_NOT_ENTITLED sengaja dipakai untuk dua kondisi
berbeda (produk tidak ada / produk ada tapi bukan hak akun Anda) — supaya kode produk milik merchant lain
tidak bisa ditebak dengan mencoba satu per satu.
Checklist Integrasi
- 01Simpan
secret_keyhanya di server, ambil token baru tepat sebelum tiap panggilan (TTL 30 detik). - 02Untuk produk pascabayar, selalu
/v1/inquirydulu dan tampilkan nominalnya ke pelanggan sebelum membayar. - 03Pakai
Idempotency-Keyyang unik per percobaan pembelian — simpan dan kirim ulang key yang sama saat retry. - 04Tangani status
unknownsebagai "belum pasti", bukan gagal — jangan buat transaksi baru untuk pesanan yang sama. - 05Verifikasi
X-Callback-Signaturesebelum memproses isi webhook apa pun. - 06Balas webhook dengan
200 OKsecepatnya, proses lanjutannya asinkron di sisi Anda. - 07Sediakan polling
GET /v1/transactions/{id}sebagai cadangan kalau webhook Anda pernah gagal 5x.