QRIS adalah standar QR pembayaran nasional Bank Indonesia. Satu QR bisa dibayar dari semua m-banking dan e-wallet (BCA mobile, BRImo, Livin', GoPay, OVO, DANA, ShopeePay, dan lainnya). Untuk website, yang dibutuhkan adalah QRIS dinamis: QR yang dibuat per transaksi dengan nominal terkunci, lalu sistem kamu diberi tahu otomatis saat pelanggan membayar.
Artikel ini menunjukkan integrasi QRIS di PHP murni memakai cURL. Tidak perlu Composer, framework, atau library tambahan. Contoh memakai API NasionalPay, tetapi polanya sama untuk payment gateway QRIS mana pun: generate → tampilkan QR → tunggu callback → verifikasi status.
Yang dibutuhkan
- Hosting PHP 7.4+ dengan ekstensi cURL aktif (hampir semua shared hosting sudah punya).
- Akun merchant NasionalPay dan satu toko yang sudah diaktifkan admin.
- Store Key toko (menu Merchant → Detail Toko). Simpan di file config, jangan di-hardcode di halaman publik.
- URL callback publik (HTTPS) yang bisa diakses dari internet.
Langkah 1: Generate QRIS dinamis
Saat pelanggan menekan tombol bayar, server kamu memanggil endpoint generate dengan nominal dan identitas pesanan. Respons berisi transaction_id, string QRIS, dan URL gambar QR yang bisa langsung ditampilkan.
<?php
// config.php — jangan commit file ini ke repo publik
define('NP_BASE', 'https://merchant.nasionalpay.com/api/payment');
define('NP_STORE_KEY', getenv('NP_STORE_KEY'));
function np_post(string $path, array $body): array {
$ch = curl_init(NP_BASE . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($body),
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
curl_close($ch);
return json_decode($raw, true) ?? [];
}
// generate.php
$order = ['id' => 'INV-1001', 'total' => 25000, 'user' => 'budi01'];
$result = np_post('/generate', [
'key' => NP_STORE_KEY,
'channel' => 'QRIS',
'amount' => $order['total'],
'player_username' => $order['user'],
]);
if (empty($result['success'])) {
exit('Gagal membuat QRIS: ' . ($result['message'] ?? 'unknown'));
}
$trx = $result['data'];
// Simpan mapping order -> transaction_id di database kamu
// UPDATE orders SET trx_id = :trx WHERE id = :id
echo '<img src="' . htmlspecialchars($trx['qris_image']) . '" alt="QRIS">';
echo '<p>Bayar Rp ' . number_format((int)$trx['amount']) . ' sebelum ' . $trx['expired_at'] . '</p>';Catatan: nominal yang harus dibayar pelanggan adalah field amount di respons (sudah termasuk markup toko jika kamu mengaktifkannya), sedangkan nett_amount adalah yang masuk ke saldo setelah MDR.
Langkah 2: Terima webhook callback
Setelah pelanggan membayar, payment gateway mengirim HTTP POST berisi JSON ke URL callback yang kamu daftarkan di panel. Di sini kamu menandai pesanan lunas. Dua aturan penting: proses harus idempoten (callback bisa datang dua kali), dan jangan percaya nominal dari callback tanpa membandingkan dengan pesanan di database kamu.
<?php
// callback.php — URL ini didaftarkan di panel merchant
require 'config.php';
$json = file_get_contents('php://input');
$callback = json_decode($json, true);
if (empty($callback['success']) || ($callback['data']['status'] ?? '') !== 'success') {
http_response_code(200);
exit(json_encode(['status' => 'ignored']));
}
$trxId = $callback['data']['transaction_id'];
$amount = (int) $callback['data']['amount'];
// 1) Cari order berdasarkan trx_id
$order = find_order_by_trx($trxId); // fungsi milikmu
if (!$order) { http_response_code(404); exit; }
// 2) Idempoten: kalau sudah lunas, jangan proses ulang
if ($order['status'] === 'paid') { exit(json_encode(['status' => 'ok'])); }
// 3) Verifikasi ulang ke server gateway (jangan hanya percaya body callback)
$check = np_post('/status', ['key' => NP_STORE_KEY, 'transaction_id' => $trxId]);
if (($check['data']['status'] ?? '') !== 'success') { http_response_code(409); exit; }
// 4) Cocokkan nominal
if ($amount < $order['total']) { http_response_code(409); exit; }
mark_order_paid($order['id']); // fungsi milikmu
echo json_encode(['status' => 'ok']);Langkah 3: Cek status manual (fallback)
Callback bisa gagal terkirim karena server kamu down atau firewall hosting. Sediakan tombol "Cek status pembayaran" di halaman pesanan yang memanggil endpoint /status dengan transaction_id. Ini juga berguna untuk polling ringan setiap 5–10 detik di halaman QR.
<?php
$status = np_post('/status', [
'key' => NP_STORE_KEY,
'transaction_id' => $_GET['trx'],
]);
// $status['data']['status'] => "pending" | "success" | "failed"Kesalahan umum yang bikin integrasi gagal
- Store Key ditaruh di JavaScript/frontend. Semua request ke gateway harus dari server.
- Callback URL memakai HTTP, bukan HTTPS, atau diblokir oleh plugin keamanan/Cloudflare rule.
- Callback membalas status 500 karena error PHP → gateway menganggap gagal. Cek log callback di panel merchant.
- Menandai order lunas hanya dari body callback tanpa cek ulang ke /status.
- Tidak menangani status failed/kedaluwarsa: QRIS dinamis punya masa berlaku; buat QR baru bila pelanggan terlambat.
Pakai SDK PHP kalau mau lebih ringkas
Kalau tidak ingin menulis cURL sendiri, NasionalPay menyediakan SDK PHP satu file (class.nasionalpay.php) yang membungkus generate, status, saldo, dan payout. Kode sumbernya terbuka di GitHub: github.com/nasionalpay/nasionalpay-php-sdk, dan bisa diunduh dari panel merchant menu Download SDK.
Berapa biayanya?
Di NasionalPay tidak ada biaya pendaftaran atau bulanan. MDR QRIS 2% per transaksi sukses (1% untuk volume besar), dipotong otomatis dari saldo. Penarikan saldo ke BCA, BRI, BNI, DANA, atau ShopeePay dikenai fee 1% + Rp4.500. Detail lengkap ada di halaman harga.