SmartPG Dokumentasi API v1 Portal Mitra

Dokumentasi API

API SmartPG memungkinkan sistem Anda membuat tagihan QRIS dan Virtual Account (BCA, BNI, BRI, Mandiri), mengecek status, dan menerima notifikasi (callback) saat pembayaran lunas.

Base URL
https://smartpgd.rizaluardi.cloud/api/v1
Format
JSON (Content-Type: application/json)
Nominal
Integer rupiah, tanpa desimal (mis. 50000)
Zona waktu
ISO 8601, Asia/Jakarta (+07:00)
Rate limit
120 request / menit per API key

Alur Integrasi

  1. Ambil API Key & Callback Secret di Portal Mitra → API & Callback, lalu atur Callback URL.
  2. Buat transaksi via POST /qris/create atau POST /va/create.
  3. Tampilkan QR / nomor VA ke customer, atau arahkan ke checkout_url (halaman bayar siap pakai).
  4. Saat customer membayar, kami kirim callback ke server Anda & saldo Anda bertambah.
  5. Sebagai cadangan, cek status via GET /transactions/{trx_id}.

Autentikasi

Setiap request wajib menyertakan API key pada header:

X-API-KEY: spg_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
Accept: application/json

Alternatif: Authorization: Bearer <api_key>. Jika Anda mengisi IP Whitelist di portal, request dari IP lain akan ditolak (IP_NOT_ALLOWED).

Simpan API key hanya di server. Jangan pernah menaruhnya di aplikasi mobile, JavaScript frontend, atau repositori publik.

Format Respons

Semua respons memakai struktur yang sama:

// Sukses
{
  "success": true,
  "code": "SUCCESS",
  "message": "QRIS berhasil dibuat",
  "data": { ... }
}

// Gagal
{
  "success": false,
  "code": "VALIDATION_ERROR",
  "message": "The amount field is required.",
  "data": { "amount": ["The amount field is required."] }
}

Biaya Admin

Setiap produk memiliki biaya admin (flat dan/atau persen) yang diatur per mitra. Biaya bisa ditanggung:

  • customer — biaya ditambahkan ke tagihan. total_amount = amount + admin_fee, Anda menerima amount.
  • merchant — tagihan tetap, biaya dipotong dari dana Anda. total_amount = amount, Anda menerima amount − admin_fee.

Nilai pasti ada di respons: admin_fee, total_amount (yang dibayar customer) dan net_amount (yang masuk ke saldo Anda).

product_codeProdukBiaya default
QRISQRISRp 500
VA_BCAVirtual Account BCARp 5.000
VA_BNIVirtual Account BNIRp 5.000
VA_BRIVirtual Account BRIRp 5.000
VA_MANDIRIVirtual Account MandiriRp 5.000

Biaya aktual akun Anda bisa berbeda — cek di Portal Mitra atau GET /products.

Daftar Produk

GET /products

Produk aktif untuk akun Anda beserta biaya & batas nominal.

{
  "success": true,
  "code": "SUCCESS",
  "message": "Success",
  "data": [
    {
      "product_code": "QRIS",
      "name": "QRIS",
      "payment_method": "qris",
      "fee_flat": 500,
      "fee_percent": 0,
      "fee_bearer": "customer",
      "min_amount": 1000,
      "max_amount": 10000000
    },
    {
      "product_code": "VA_BCA",
      "name": "Virtual Account BCA",
      "payment_method": "va",
      "fee_flat": 5000,
      "fee_percent": 0,
      "fee_bearer": "customer",
      "min_amount": 10000,
      "max_amount": 50000000
    }
  ]
}

Buat QRIS

POST /qris/create

Membuat QRIS dinamis (MPM) dengan nominal tertentu. QR berlaku ±15 menit.

Request Body

FieldTipeWajibKeterangan
reference_idstring(64)YaID unik dari sistem Anda (no. invoice). Hanya huruf, angka, -_.. Tidak boleh dipakai ulang.
amountintegerYaNominal tagihan (sebelum biaya admin).
customer_namestringTidakNama customer.
customer_phonestringTidakNo. HP customer (angka).
customer_emailstringTidakEmail customer.
descriptionstringTidakKeterangan / nama order.
callback_urlurlTidakOverride callback URL khusus transaksi ini.
metadataobjectTidakData bebas, dikembalikan apa adanya di status & callback.

Contoh Request

curl -X POST https://smartpgd.rizaluardi.cloud/api/v1/qris/create \
  -H "X-API-KEY: spg_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "reference_id": "INV-2026-0001",
    "amount": 50000,
    "customer_name": "Budi",
    "customer_phone": "081234567890",
    "description": "Top up 50K",
    "metadata": { "user_id": 77 }
  }'

Respons 201

{
  "success": true,
  "code": "SUCCESS",
  "message": "QRIS berhasil dibuat",
  "data": {
    "trx_id": "SPGQR261011143005AB12CD",
    "reference_id": "INV-2026-0001",
    "product_code": "QRIS",
    "status": "PENDING",
    "amount": 50000,
    "admin_fee": 500,
    "fee_bearer": "customer",
    "total_amount": 50500,
    "net_amount": 50000,
    "qr_content": "00020101021226670016COM.NOBUBANK.WWW...6304ABCD",
    "qr_image_url": "https://.../qr/SPGQR261011143005AB12CD.svg",
    "checkout_url": "https://.../pay/SPGQR261011143005AB12CD",
    "expired_at": "2026-10-11T14:45:05+07:00",
    "created_at": "2026-10-11T14:30:05+07:00"
  }
}

Render qr_content menjadi QR code di aplikasi Anda, atau langsung gunakan qr_image_url (SVG) / checkout_url.

Buat Virtual Account

POST /va/create

Membuat nomor Virtual Account sekali pakai (closed amount — customer wajib membayar persis sebesar total_amount). Berlaku ±24 jam.

Request Body

FieldTipeWajibKeterangan
bankstringYaKode bank: BCA, BNI, BRI, MANDIRI. Lihat produk aktif akun Anda di GET /products.
reference_idstring(64)YaID unik dari sistem Anda.
amountintegerYaNominal tagihan (sebelum biaya admin).
customer_namestring(40)YaNama yang tampil di VA.
customer_phonestringTidakNo. HP customer.
customer_email, description, callback_url, metadataTidakSama seperti QRIS.

Contoh Request

curl -X POST https://smartpgd.rizaluardi.cloud/api/v1/va/create \
  -H "X-API-KEY: spg_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "bank": "BCA",
    "reference_id": "INV-2026-0002",
    "amount": 150000,
    "customer_name": "Budi Santoso",
    "customer_phone": "081234567890"
  }'

Respons 201

{
  "success": true,
  "code": "SUCCESS",
  "message": "Virtual Account berhasil dibuat",
  "data": {
    "trx_id": "SPGVA261011143210EF34GH",
    "reference_id": "INV-2026-0002",
    "product_code": "VA_BCA",
    "status": "PENDING",
    "amount": 150000,
    "admin_fee": 5000,
    "fee_bearer": "customer",
    "total_amount": 155000,
    "net_amount": 150000,
    "bank": "BCA",
    "va_number": "1234500012345678",
    "va_name": "Budi Santoso",
    "checkout_url": "https://.../pay/SPGVA261011143210EF34GH",
    "expired_at": "2026-10-12T14:32:10+07:00",
    "created_at": "2026-10-11T14:32:10+07:00"
  }
}

Cek Status Transaksi

GET /transactions/{trx_id}

Bisa memakai trx_id atau reference_id Anda. Jika masih PENDING, sistem otomatis mengecek ke provider (nonaktifkan dengan ?sync=0).

{
  "success": true,
  "code": "SUCCESS",
  "message": "Success",
  "data": {
    "trx_id": "SPGQR261011143005AB12CD",
    "reference_id": "INV-2026-0001",
    "product_code": "QRIS",
    "payment_method": "qris",
    "status": "PAID",
    "amount": 50000,
    "admin_fee": 500,
    "fee_bearer": "customer",
    "total_amount": 50500,
    "net_amount": 50000,
    "qris": {
      "qr_content": "000201010212...",
      "qr_image_url": "https://.../qr/SPGQR261011143005AB12CD.svg"
    },
    "customer": { "name": "Budi", "phone": "081234567890", "email": null },
    "description": "Top up 50K",
    "metadata": { "user_id": 77 },
    "expired_at": "2026-10-11T14:45:05+07:00",
    "paid_at": "2026-10-11T14:31:40+07:00",
    "created_at": "2026-10-11T14:30:05+07:00"
  }
}

Untuk VA, objek qris diganti virtual_account: {"bank","va_number","biller_code","va_name"}.

Mandiri memakai Bill Payment: tampilkan biller_code (Kode Perusahaan) dan va_number (Kode Bayar) ke customer. Pada bank lain biller_code bernilai null.

Daftar Transaksi

GET /transactions?status=PAID&date_from=2026-10-01&per_page=20&page=1

Query opsional: status, reference_id, date_from, date_to (YYYY-MM-DD), per_page (maks 100), page.

{
  "success": true,
  "code": "SUCCESS",
  "message": "Success",
  "data": {
    "items": [ { ...objek transaksi... } ],
    "pagination": { "page": 1, "per_page": 20, "total": 125, "last_page": 7 }
  }
}

Cek Saldo

GET /balance
{
  "success": true,
  "code": "SUCCESS",
  "message": "Success",
  "data": { "merchant_code": "M00001", "balance": 1250000, "currency": "IDR" }
}

Callback (Webhook)

Saat status transaksi berubah menjadi PAID, EXPIRED, atau FAILED, kami mengirim POST JSON ke Callback URL Anda.

Headers

Content-Type: application/json
X-Callback-Event: payment.paid
X-Callback-Timestamp: 1791707500
X-Callback-Signature: 5f2c1e...e9a0   (hex HMAC-SHA256)
X-Merchant-Code: M00001

Body

{
  "event": "payment.paid",
  "data": {
    "trx_id": "SPGVA261011143210EF34GH",
    "reference_id": "INV-2026-0002",
    "product_code": "VA_BCA",
    "payment_method": "va",
    "status": "PAID",
    "amount": 150000,
    "admin_fee": 5000,
    "fee_bearer": "customer",
    "total_amount": 155000,
    "net_amount": 150000,
    "virtual_account": { "bank": "BCA", "va_number": "1234500012345678", "va_name": "Budi Santoso" },
    "customer": { "name": "Budi Santoso", "phone": "081234567890", "email": null },
    "description": null,
    "metadata": null,
    "expired_at": "2026-10-12T14:32:10+07:00",
    "paid_at": "2026-10-11T15:02:51+07:00",
    "created_at": "2026-10-11T14:32:10+07:00"
  }
}

Aturan

  • Balas dengan HTTP 2xx dalam waktu < 15 detik. Selain itu dianggap gagal.
  • Callback gagal dicoba ulang otomatis hingga 5x (1, 5, 15, 60 menit). Anda juga bisa kirim ulang manual dari portal.
  • Callback bisa terkirim lebih dari sekali — proses secara idempoten berdasarkan trx_id / reference_id.
  • Event: payment.paid, payment.expired, payment.failed.

Verifikasi Signature Callback

Wajib diverifikasi untuk memastikan callback benar-benar dari kami:

signature = hex( HMAC_SHA256( key = CALLBACK_SECRET, message = X-Callback-Timestamp + "." + RAW_BODY ) )

Gunakan raw body persis seperti diterima (jangan di-decode lalu di-encode ulang). Tolak jika timestamp selisih > 5 menit.

PHP

$raw       = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_CALLBACK_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_CALLBACK_SIGNATURE'] ?? '';
$secret    = 'sk_xxx'; // Callback Secret dari portal

$expected = hash_hmac('sha256', $timestamp . '.' . $raw, $secret);

if (!hash_equals($expected, $signature) || abs(time() - (int) $timestamp) > 300) {
    http_response_code(401);
    exit('invalid signature');
}

$payload = json_decode($raw, true);
if ($payload['event'] === 'payment.paid') {
    // cari order by $payload['data']['reference_id'], tandai lunas jika belum
}

http_response_code(200);
echo 'OK';

Node.js (Express)

const crypto = require('crypto');

app.post('/callback/spg', express.raw({ type: 'application/json' }), (req, res) => {
  const ts  = req.get('X-Callback-Timestamp');
  const sig = req.get('X-Callback-Signature');
  const expected = crypto.createHmac('sha256', process.env.SPG_CALLBACK_SECRET)
    .update(ts + '.' + req.body.toString('utf8'))
    .digest('hex');

  if (sig.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return res.status(401).send('invalid signature');
  }

  const payload = JSON.parse(req.body);
  // proses payload.data secara idempoten
  res.send('OK');
});

Status Transaksi

StatusArti
PENDINGMenunggu pembayaran.
PAIDLunas. Saldo sudah masuk ke akun Anda (net_amount).
EXPIREDMelewati batas waktu, tidak bisa dibayar lagi.
FAILEDGagal dibuat / ditolak provider.

Kode Error

HTTPcodeArti
401UNAUTHORIZEDAPI key tidak ada / tidak valid.
403MERCHANT_SUSPENDEDAkun mitra dinonaktifkan.
403MERCHANT_NOT_VERIFIEDData perusahaan belum diverifikasi admin.
403IP_NOT_ALLOWEDIP tidak ada di whitelist.
403PRODUCT_NOT_AVAILABLEProduk tidak aktif untuk akun Anda.
404NOT_FOUNDTransaksi / endpoint tidak ditemukan.
409DUPLICATE_REFERENCEreference_id sudah pernah dipakai (termasuk transaksi gagal). data.trx_id berisi transaksi yang ada.
422VALIDATION_ERRORParameter tidak valid. Detail di data.
422INVALID_AMOUNTNominal di luar batas min/max produk.
429RATE_LIMITEDMelebihi batas request.
502PROVIDER_ERRORTagihan gagal dibuat. Coba lagi dengan reference_id baru.
503SERVICE_UNAVAILABLEProduk sedang tidak tersedia sementara. Coba lagi nanti.
500SERVER_ERRORKesalahan internal.

Penarikan Saldo

Penarikan dilakukan lewat Portal Mitra → Tarik Saldo (tidak melalui API):

  1. Isi nominal & rekening tujuan, lalu ajukan. Saldo langsung ditahan (dipotong) sebesar nominal + biaya penarikan.
  2. Admin memproses transfer secara manual ke rekening Anda.
  3. Status menjadi SUCCESS (beserta referensi transfer), atau REJECTED dan saldo dikembalikan.
  4. Selama masih PENDING, Anda bisa membatalkan dan saldo kembali utuh.

Checklist Go-Live

  • API key disimpan di server (env/secret manager), bukan di frontend.
  • IP Whitelist diisi dengan IP server produksi Anda.
  • Callback URL HTTPS, membalas 2xx cepat, dan memverifikasi signature.
  • Pemrosesan callback idempoten (cek apakah order sudah lunas sebelum update).
  • reference_id unik untuk setiap transaksi.
  • Ada mekanisme cek status (GET /transactions/{id}) untuk order yang belum menerima callback.