Tutorial Pasang

Panduan ini buat developer brand (misalnya website AeroConvo atau AbsenKuy) yang mau menerima pembayaran QRIS atau transfer bank (Virtual Account) lewat QRISKu. Alurnya: server kamu minta pembayaran ke API, pembeli bayar di halaman checkout, lalu kami kabari server kamu lewat webhook.

Tentang nama di aplikasi bank. Semua QRIS dan Virtual Account dibuat atas akun DOKU resmi ARENAGO. Saat pembeli scan QRIS atau transfer ke nomor VA, aplikasi bank/e-wallet menampilkan nama ARENAGO. Nama brand kamu tampil di halaman checkout QRISKu, bukan di aplikasi bank. Kami tidak mengubah data QR.

#1. Ambil API key

  1. Minta admin QRISKu membuatkan brand dan login Portal Brand untuk kamu.
  2. Masuk ke Portal Brand, buka menu API & Webhook.
  3. Klik Lihat di baris API key, lalu Salin. Lakukan juga untuk Webhook secret.
  4. Simpan keduanya di file .env server kamu:
QRISKU_BASE_URL=https://pay.arenago.app
QRISKU_API_KEY=qk_live_xxxxxxxxxxxxxxxx
QRISKU_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxx

Jangan taruh API key di kode frontend (JavaScript browser, aplikasi Android) atau repo publik. Kalau bocor, klik Buat baru di Portal Brand dan kunci lama langsung mati.

Semua endpoint memakai POST dengan header X-API-Key: <api key> (atau Authorization: Bearer <api key>). Body boleh JSON atau form (x-www-form-urlencoded). Base URL API: https://pay.arenago.app/api/v1.

#2. Buat pembayaran

POST /api/v1/payments/create

Field Wajib Keterangan
amount ya Nominal dalam rupiah, angka bulat, Rp 1.000 sampai Rp 10.000.000
description tidak Keterangan yang tampil di checkout, maks. 200 karakter
order_id tidak ID order dari sistem kamu, maks. 64 karakter. Tidak bisa dipakai dua kali selama transaksi lama masih menunggu atau sudah lunas
fee_by tidak buyer (default, biaya layanan ditambahkan ke total) atau merchant (dipotong dari saldo brand)
callback_url tidak URL webhook khusus transaksi ini. Kalau kosong, pakai URL webhook di Portal Brand
return_url tidak Ke mana pembeli diarahkan 3 detik setelah bayar
test tidak Isi 1 untuk memaksa mode sandbox (QR tes yang tidak bisa dibayar)
method tidak qris (default) atau va (transfer bank lewat Virtual Account). Lihat Metode pembayaran
bank kalau method = va Kode bank VA: bni, btn, bnc
create-payment.sh
curl -X POST https://pay.arenago.app/api/v1/payments/create \
  -H "X-API-Key: $QRISKU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 10000,
    "description": "Top up saldo",
    "order_id": "INV-1001",
    "fee_by": "buyer",
    "return_url": "https://tokokamu.com/terima-kasih"
  }'

Contoh respon:

{
  "success": true,
  "data": {
    "transaction_id": "TXMH7Q2K1A9F3C2B",
    "order_id": "INV-1001",
    "description": "Top up saldo",
    "status": "pending",
    "mode": "live",
    "amount": 10000,
    "fee": 70,
    "fee_by": "buyer",
    "total_amount": 10070,
    "net_amount": 10000,
    "method": "qris",
    "va_bank": null,
    "va_bank_name": null,
    "va_number": null,
    "qr_string": "00020101021226...",
    "qr_image_url": "https://pay.arenago.app/pay/TXMH7Q2K1A9F3C2B/qr.png",
    "payment_url": "https://pay.arenago.app/pay/TXMH7Q2K1A9F3C2B",
    "expires_at": "2026-10-26T08:45:00.000Z",
    "paid_at": null,
    "created_at": "2026-10-26T08:30:00.000Z"
  }
}

Cara paling gampang: arahkan pembeli ke payment_url. Halaman itu sudah berisi QR, total, hitung mundur, dan berubah otomatis begitu lunas. Mau tampilkan QR di halaman sendiri? Pakai qr_image_url (PNG) atau buat gambar QR dari qr_string.

total_amount adalah yang dibayar pembeli. net_amount adalah yang masuk ke saldo brand. QR berlaku 15 menit (expires_at, format UTC).

#2a. Metode pembayaran: QRIS atau Virtual Account

Tanpa field method, semuanya sama seperti sebelumnya: pembayaran QRIS. Integrasi lama tidak perlu diubah.

Halaman checkout (payment_url) punya dua tab: QRIS dan Transfer Bank (VA). Di tab VA, pembeli memilih bank, lalu nomor Virtual Account dibuat saat itu juga untuk transaksi yang sama (ID transaksi tidak berubah). Jadi cara paling gampang tetap: arahkan pembeli ke payment_url dan biarkan mereka memilih.

Mau langsung buat nomor VA dari server kamu (misalnya untuk ditampilkan di aplikasi sendiri)? Kirim method dan bank:

curl -X POST https://pay.arenago.app/api/v1/payments/create \
  -H "X-API-Key: $QRISKU_API_KEY" -H "Content-Type: application/json" \
  -d '{"amount": 10000, "order_id": "INV-1002", "method": "va", "bank": "bni"}'

Respon berisi field yang sama, ditambah nomor VA:

{
  "success": true,
  "data": {
    "transaction_id": "TXMH7Q2K1A9F3C2C",
    "status": "pending",
    "total_amount": 10070,
    "method": "va",
    "va_bank": "bni",
    "va_bank_name": "BNI",
    "va_number": "9882917231250453",
    "payment_url": "https://pay.arenago.app/pay/TXMH7Q2K1A9F3C2C",
    "expires_at": "2026-10-26T08:45:00.000Z",
    "...": "..."
  }
}

Yang perlu diketahui:

#3. Cek status pembayaran

POST /api/v1/payments/status dengan transaction_id atau order_id. Kamu hanya bisa melihat transaksi milik brand kamu sendiri.

check-status.sh
curl -X POST https://pay.arenago.app/api/v1/payments/status \
  -H "X-API-Key: $QRISKU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"transaction_id": "TX..."}'
# atau cari pakai order_id kamu sendiri: -d '{"order_id": "INV-1001"}'

Nilai data.status:

Status Arti
pending Menunggu dibayar
paid Lunas, saldo brand sudah bertambah
expired Lewat batas waktu, tidak dibayar. Buat pembayaran baru
failed Gagal

Webhook tetap cara utama. Cek status berguna sebagai cadangan, misalnya saat pembeli kembali ke return_url atau untuk job pengecekan berkala.

#4. Terima webhook

Setiap status berubah, kami kirim POST JSON ke URL webhook kamu:

{
  "event": "payment.success",
  "data": {
    "transaction_id": "TX...",
    "order_id": "INV-1001",
    "description": "Top up saldo",
    "status": "paid",
    "mode": "live",
    "amount": 10000,
    "fee": 70,
    "fee_by": "buyer",
    "total_amount": 10070,
    "net_amount": 10000,
    "method": "va",
    "va_bank": "bni",
    "va_bank_name": "BNI",
    "va_number": "9882917231250453",
    "payment_url": "https://pay.arenago.app/pay/TX...",
    "paid_at": "2026-10-26T08:31:10.000Z",
    "...": "..."
  },
  "sent_at": "2026-10-26T08:31:12.000Z"
}

Field method berisi qris atau va (cara yang dipakai pembeli). Untuk va, va_bank, va_bank_name, dan va_number ikut dikirim. Untuk qris ketiganya null. Field qr_string tidak dikirim di webhook.

Event: payment.success, payment.expired, payment.failed, dan webhook.test (dari tombol Kirim webhook tes). URL tujuan dicatat saat transaksi dibuat, jadi kalau URL webhook di Portal Brand diganti, perubahan berlaku untuk transaksi baru. Pengiriman yang gagal (bukan HTTP 2xx) dicoba ulang 3 kali: setelah 10 detik, 1 menit, dan 5 menit. Webhook yang sama bisa datang lebih dari sekali, jadi proses secara idempotent.

Header yang dikirim:

Header Isi
X-QRISKu-Event Nama event
X-QRISKu-Timestamp Unix time (detik) saat dikirim
X-QRISKu-Signature sha256= + HEX(HMAC-SHA256(webhook_secret, timestamp + "." + body mentah))

Wajib verifikasi signature sebelum percaya isinya. Hitung HMAC dari body mentah persis seperti diterima, bandingkan dengan perbandingan aman waktu (hash_equals / timingSafeEqual), dan tolak timestamp yang lebih tua dari 5 menit.

webhook.php
<?php
// webhook.php — taruh di https://tokokamu.com/qris-webhook.php lalu isi URL itu di Portal Brand.
$secret = getenv('QRISKU_WEBHOOK_SECRET'); // whsec_...

$raw       = file_get_contents('php://input'); // body mentah, JANGAN pakai $_POST
$timestamp = $_SERVER['HTTP_X_QRISKU_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_QRISKU_SIGNATURE'] ?? '';

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

$payload = json_decode($raw, true);
$event   = $payload['event'];
$data    = $payload['data'];

if ($event === 'payment.success') {
    // Update order sekali saja (idempotent), contoh:
    // UPDATE orders SET status='paid' WHERE order_id = ? AND status <> 'paid'
    error_log("LUNAS: {$data['order_id']} Rp{$data['total_amount']}");
} elseif ($event === 'payment.expired' || $event === 'payment.failed') {
    error_log("BATAL: {$data['order_id']} ({$data['status']})");
}

http_response_code(200);
echo 'ok';

Aturan pengiriman:

#5. Kode status dan error

Semua respon API berbentuk {"success": true, "data": {...}} atau {"success": false, "error": "pesan"}.

HTTP Arti Yang perlu dilakukan
200 Berhasil
400 Input tidak valid (nominal di luar batas, URL salah, method/bank tidak dikenal, dll.) Baca error, perbaiki request
401 API key salah, kosong, atau brand nonaktif Cek header X-API-Key
403 Aksi tidak diizinkan (misalnya simulasi bayar di transaksi real)
404 Transaksi tidak ditemukan (atau milik brand lain) Cek transaction_id / order_id
409 order_id sudah dipakai transaksi yang masih aktif/lunas Pakai order_id baru
413 Body terlalu besar
429 Terlalu banyak request (120/menit per API key) Tunggu sesuai header Retry-After
502 Penyedia pembayaran (DOKU) sedang bermasalah, atau VA bank itu gagal dibuat Coba lagi beberapa saat, atau pakai bank lain / QRIS
503 Mode real belum siap di sisi platform Hubungi admin QRISKu

#6. Contoh integrasi lengkap: tombol "Bayar pakai QRIS"

Alur: pembeli klik tombol, server kamu buat order + pembayaran, pembeli diarahkan ke halaman QRIS, setelah bayar kembali ke return_url, dan order ditandai lunas oleh webhook.

checkout.php
<?php
// checkout.php — tombol "Bayar pakai QRIS" sederhana.
require __DIR__ . '/create-payment.php'; // fungsi qrisku_request()

$product = ['name' => 'Paket Premium 1 Bulan', 'price' => 49000];

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $orderId = 'ORD-' . time(); // simpan order ini di database kamu dengan status "menunggu"
    try {
        $p = qrisku_request('/api/v1/payments/create', [
            'amount'      => $product['price'],
            'description' => $product['name'],
            'order_id'    => $orderId,
            'return_url'  => 'https://tokokamu.com/terima-kasih.php?order=' . $orderId,
        ]);
        header('Location: ' . $p['payment_url'], true, 303); // ke halaman QRIS
        exit;
    } catch (Exception $e) {
        http_response_code(502);
        exit('Gagal membuat pembayaran: ' . htmlspecialchars($e->getMessage()));
    }
}
?>
<!doctype html><meta charset="utf-8"><title>Toko Contoh</title>
<h1><?= htmlspecialchars($product['name']) ?></h1>
<p>Harga: Rp <?= number_format($product['price'], 0, ',', '.') ?></p>
<form method="post"><button>Bayar pakai QRIS</button></form>

Jangan tandai order lunas hanya karena pembeli sampai di return_url. Status final selalu dari webhook (atau cek status).

#7. Endpoint lain

Endpoint Fungsi
POST /api/v1/payment-methods Metode yang aktif: methods (["qris","va"]) dan va_banks ([{"code":"bni","name":"BNI"}, ...])
POST /api/v1/balance Saldo brand: balance (real, bisa ditarik) dan sandbox_balance (tes)
POST /api/v1/withdrawals/create Ajukan penarikan: amount, bank_name, account_no, account_name. Minimal Rp 10.000
POST /api/v1/payments/simulate-paid Khusus transaksi sandbox: tandai lunas untuk tes webhook
curl -X POST https://pay.arenago.app/api/v1/balance -H "X-API-Key: $QRISKU_API_KEY"

#8. Uji coba di sandbox

  1. Buat pembayaran dengan "test": 1. QR yang keluar sengaja tidak valid dan tidak bisa dibayar, jadi aman.
  2. Tandai lunas lewat tombol Tandai Lunas (Tes) di halaman checkout atau Portal Brand, atau panggil simulate-paid.
  3. Pastikan webhook kamu menerima payment.success dan signature-nya valid.
  4. Saldo tes masuk ke sandbox_balance dan tidak bisa ditarik.

#9. Checklist sebelum live