Dokumentasi API · v1

Integrasikan layanan PPOB kami ke sistem Anda

Panduan lengkap untuk merchant: autentikasi, transaksi prabayar & pascabayar, saldo, dan notifikasi webhook — dengan contoh permintaan dan respons asli.

Base URL http://217.15.165.213
01

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

  1. Tukar api_key + secret_key Anda menjadi token lewat POST /v1/auth.
  2. (Khusus produk pascabayar) Cek tagihan lewat POST /v1/inquiry untuk tahu nominal sebenarnya.
  3. Buat transaksi lewat POST /v1/transactions — dengan atau tanpa callback_url.
  4. Pantau hasilnya: lewat respons langsung (mode sinkron), polling GET /v1/transactions/{id}, atau menerima webhook (mode asinkron).
02

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.

POST /v1/auth
Request body
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
Contoh permintaan
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
}
Token hanya berlaku 30 detik. Ini bukan token sesi untuk dipakai berjam-jam — ambil token baru tepat sebelum memanggil endpoint lain, dan jangan menyimpannya untuk dipakai ulang nanti. Kredensial 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.

03

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 nulldata dan error tidak pernah keduanya terisi sekaligus. Lihat daftar lengkap kode error untuk arti tiap error.code.

04

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.

Nominal dari /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.
05

Daftar Produk

Katalog produk yang tersedia untuk akun Anda — hanya produk yang sudah di-entitle ke merchant Anda yang muncul di sini.

GET /v1/products butuh auth
Contoh respons — 200 OK
{
  "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.

06

Cek Tagihan

Khusus produk pascabayar. Mengembalikan nominal yang akan ditagihkan beserta informasi pelanggan dari biller — tanpa membuat transaksi apa pun.

POST /v1/inquiry butuh auth
Request body
Field Tipe Keterangan
product string Kode produk dari /v1/products.wajib
billNumber string Nomor pelanggan/meter/tagihan.wajib
Contoh respons — 200 OK
{
  "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.

07

Buat Transaksi

Endpoint inti — membeli produk prabayar atau membayar tagihan pascabayar. Perilakunya bercabang dua tergantung apakah Anda mengirim callback_url.

POST /v1/transactions butuh auth
Header
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
Request body
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.

Contoh permintaan
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.

08

Detail Transaksi

Ambil status transaksi kapan saja setelah dibuat — dipakai untuk polling pada mode sinkron, atau mengonfirmasi ulang isi webhook yang Anda terima.

GET /v1/transactions/{id} butuh auth

{id} adalah transaction_id yang dikembalikan saat transaksi dibuat. Bentuk respons identik dengan Buat Transaksi — lihat contoh di sana.

Transaksi milik merchant lain — atau ID yang sama sekali tidak ada — keduanya menjawab 404 TRANSACTION_NOT_FOUND yang identik, supaya ID transaksi tidak bisa dipakai menebak-nebak data merchant lain.
09

Saldo

GET /v1/wallet/balance butuh auth
Contoh respons — 200 OK
{
  "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.

10

Mutasi Saldo

GET /v1/wallet/statement butuh auth
Query parameter
Field Tipe Keterangan
limit int Default 20, maksimum 100.
offset int Default 0.
Contoh respons — 200 OK
{
  "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
}
12

Webhook

Bila Anda mengirim callback_url saat membuat transaksi, kami mengirim satu notifikasi POST ke URL tersebut begitu transaksi mencapai status final.

Header yang dikirim
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.
Contoh payload
{
  "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.

Setelah 5 kali gagal, pengiriman webhook berhenti — transaksi tetap tersimpan dengan status finalnya, jadi selalu sediakan jalur cadangan: polling GET /v1/transactions/{id} secara berkala untuk transaksi yang Anda buat secara asinkron.
13

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.

14

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.

15

Checklist Integrasi

  1. 01Simpan secret_key hanya di server, ambil token baru tepat sebelum tiap panggilan (TTL 30 detik).
  2. 02Untuk produk pascabayar, selalu /v1/inquiry dulu dan tampilkan nominalnya ke pelanggan sebelum membayar.
  3. 03Pakai Idempotency-Key yang unik per percobaan pembelian — simpan dan kirim ulang key yang sama saat retry.
  4. 04Tangani status unknown sebagai "belum pasti", bukan gagal — jangan buat transaksi baru untuk pesanan yang sama.
  5. 05Verifikasi X-Callback-Signature sebelum memproses isi webhook apa pun.
  6. 06Balas webhook dengan 200 OK secepatnya, proses lanjutannya asinkron di sisi Anda.
  7. 07Sediakan polling GET /v1/transactions/{id} sebagai cadangan kalau webhook Anda pernah gagal 5x.