Tutorial integrasi pembayaran QRIS di website PHP dan Laravel
Langkah demi langkah menambahkan pembayaran QRIS otomatis ke website PHP atau Laravel: buat tagihan lewat API, terima webhook, lalu uji di mode uji.
Tim WaroengPay · Diperbarui · 9 menit baca
Tutorial ini menambahkan pembayaran QRIS otomatis ke website PHP: pembeli checkout, membayar QRIS dari aplikasi bank atau e-wallet apa pun, lalu pesanannya berubah lunas sendiri tanpa kamu mengecek mutasi. Contohnya memakai PHP 8 dengan ekstensi cURL, dan versi Laravel ada di bagian akhir.
Alurnya
- Pembeli checkout, lalu servermu membuat tagihan lewat
POST /api/payments. - Servermu menyimpan
idtagihan di pesanan dan mengarahkan pembeli kepay_url. - Pembeli memindai QRIS yang nominalnya sudah terisi, lalu membayar.
- Begitu dananya terbaca di mutasi rekening, WaroengPay mengirim webhook
payment.paid. Servermu memverifikasinya lalu menandai pesanan lunas.
1. Siapkan API key mode uji
Buat akun WaroengPay. Setelah akunmu disetujui, buka dashboard → Integrasi & API → Kredensial dan buat API key mode uji (wp_test_...). Secret webhook mode uji ada di bagian Webhook dan tampil setelah kamu memasukkan password akun. Simpan keduanya sebagai variabel lingkungan di server, bukan di kode yang ikut ke browser atau repository:
WAROENGPAY_API_KEY=wp_test_xxxxxxxxxxxxxxxxWAROENGPAY_WEBHOOK_SECRET=whsec_test_xxxxxxxxxxxxxxxx
Dengan key mode uji, semua langkah di bawah bisa dicoba tanpa uang sungguhan. Tagihan uji tidak bisa dibayar dengan aplikasi apa pun; kamu melunasinya lewat simulasi.
2. Fungsi kecil untuk memanggil API
<?php// Panggil API WaroengPay dari server. API key tidak boleh sampai ke browser.function waroengpay(string $method, string $path, ?array $body = null, array $headers = []): array{$ch = curl_init("https://waroengpay.com" . $path);curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method,CURLOPT_RETURNTRANSFER => true,CURLOPT_TIMEOUT => 15,CURLOPT_HTTPHEADER => array_merge(["Authorization: Bearer " . getenv("WAROENGPAY_API_KEY"),"Content-Type: application/json",], $headers),]);if ($body !== null) {curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));}$raw = curl_exec($ch);$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);$data = is_string($raw) ? json_decode($raw, true) : null;if ($status >= 400 || !is_array($data)) {throw new RuntimeException($data["error"] ?? "WaroengPay tidak bisa dihubungi");}return $data;}
3. Buat tagihan saat checkout
Panggil API setelah pesanan tersimpan di databasemu. Kirim total pesanan dalam rupiah, antara Rp1.000 dan Rp10.000.000. Kode unik ditambahkan otomatis.
<?phprequire "waroengpay.php";// $order = pesanan yang baru disimpan di databasemu$tagihan = waroengpay("POST", "/api/payments", ["amount" => (int) $order["total"], // rupiah, belum termasuk kode unik"reference" => $order["number"], // mis. INV-2041"description" => "Pesanan " . $order["number"],"redirect_url" => "https://tokoku.id/pesanan/" . $order["number"],], ["Idempotency-Key: " . $order["number"]]); // terklik dua kali = tagihan yang samasave_waroengpay_id($order["id"], $tagihan["id"]); // WRG-..., kunci untuk mencocokkan webhookheader("Location: " . $tagihan["pay_url"]);exit;
Tiga hal penting di sini:
- Simpan
idtagihan (WRG-...) di pesananmu. Webhook nanti dicocokkan lewat id ini, bukan lewatreference. - Header
Idempotency-Keyberisi nomor pesanan membuat request yang terulang, misalnya tombol terklik dua kali atau koneksi putus, mengembalikan tagihan yang sama, bukan tagihan baru. - Nominal yang harus dibayar ada di
amountdan sudah termasuk kode unik. Kalau kamu menampilkannya sendiri, tampilkan apa adanya.
4. Tampilkan pembayaran
Cara termudah adalah mengarahkan pembeli ke pay_url seperti contoh di atas. Halaman bayar WaroengPay sudah berisi QR, nominal, hitung mundur, dan logo tokomu, lalu berubah sendiri begitu lunas. Kalau kamu mengisi redirect_url, pembeli diarahkan kembali ke tokomu 6 detik setelah lunas.
Mau menampilkan QR di halaman checkout sendiri? Buat gambar QR dari string qris di respons memakai pustaka QR apa pun, tampilkan amount persis, lalu cek statusnya secara berkala, cukup tiap 5 sampai 10 detik:
<?php$tagihan = waroengpay("GET", "/api/payments/" . $order["waroengpay_id"]);echo $tagihan["status"]; // pending | paid | expired | cancelled
redirect_url. Alamat itu bisa dibuka siapa saja. Status lunas hanya boleh berasal dari webhook atau dari API.5. Terima webhook pembayaran
Buat satu endpoint di servermu, misalnya https://tokoku.id/webhook/waroengpay. Daftarkan sebagai URL default di dashboard → Integrasi & API → Webhook, atau kirim per tagihan lewat callback_url. Isi endpoint-nya:
<?php// Dipanggil server WaroengPay, bukan oleh pembeli.$secret = getenv("WAROENGPAY_WEBHOOK_SECRET");$body = file_get_contents("php://input"); // body mentah, jangan diubah dulu$ts = $_SERVER["HTTP_X_WAROENGPAY_TIMESTAMP"] ?? "";$sig = $_SERVER["HTTP_X_SIGNATURE"] ?? "";$expected = hash_hmac("sha256", $ts . "." . $body, $secret);if (!hash_equals($expected, $sig) || abs(time() - (int) $ts) > 300) {http_response_code(401);exit;}$event = json_decode($body, true);$testMode = str_starts_with((string) getenv("WAROENGPAY_API_KEY"), "wp_test_");if (($event["livemode"] ?? true) === false && !$testMode) { // event mode uji tidak boleh menyentuh pesanan aslihttp_response_code(200);exit;}if ($event["event"] === "payment.paid") {$order = find_order_by_waroengpay_id($event["data"]["id"]); // id tagihan, BUKAN referenceif ($order && !$order["paid_at"] && (int) $order["total"] === $event["data"]["base_amount"]) {mark_paid($order["id"]); // sudah lunas = event duplikat, dilewati}}http_response_code(200);
Kode ini memverifikasi tanda tangan HMAC dari body mentah, menolak kiriman yang lebih tua dari 5 menit, lalu menandai pesanan lunas hanya kalau data.id dan nominalnya cocok. Event mode uji hanya diproses selama servermu memakai key wp_test_, jadi server produksi tidak pernah terpengaruh. Ganti find_order_by_waroengpay_id dan mark_paid dengan fungsi databasemu. Penjelasan lengkap setiap langkahnya ada di panduan webhook pembayaran.
6. Uji dengan mode uji
Webhook tagihan uji hanya dikirim ke callback_url yang diisi saat membuat tagihan; URL webhook default akun tidak dipakai. Jadi saat menguji, tambahkan callback_url ke data tagihan di langkah 3:
"callback_url" => "https://tokoku.id/webhook/waroengpay",
Lalu buka pay_url tagihan uji dan tekan Simulasikan bayar, atau panggil endpoint simulasi:
curl -X POST https://waroengpay.com/api/payments/WRG-7K2M9QX4BD1PZ/simulate \-H "Authorization: Bearer $WAROENGPAY_TEST_KEY" \-H "Content-Type: application/json" \-d '{"action":"pay"}' # atau "expire"
Tagihan berubah menjadi paid dan webhook payment.paid dengan livemode: false dikirim ke servermu. Karena servermu memakai key wp_test_, kode di langkah 5 memprosesnya seperti pembayaran sungguhan.
Servermu masih di komputer lokal? Alamat localhost tidak bisa menerima webhook. Pakai layanan tunnel seperti ngrok atau Cloudflare Tunnel untuk mendapat alamat publik sementara.
7. Versi Laravel
Simpan kredensial di config/services.php:
'waroengpay' => ['key' => env('WAROENGPAY_API_KEY'),'webhook_secret' => env('WAROENGPAY_WEBHOOK_SECRET'),],
Buat tagihan dengan HTTP client bawaan Laravel:
use Illuminate\Support\Facades\Http;public function pay(Order $order){$tagihan = Http::withToken(config('services.waroengpay.key'))->withHeaders(['Idempotency-Key' => $order->number])->timeout(15)->post('https://waroengpay.com/api/payments', ['amount' => (int) $order->total,'reference' => $order->number,'redirect_url' => route('orders.show', $order),])->throw()->json();$order->update(['waroengpay_id' => $tagihan['id']]);return redirect()->away($tagihan['pay_url']);}
Terima webhook di routes/web.php:
use App\Models\Order;use Illuminate\Http\Request;Route::post('/webhook/waroengpay', function (Request $request) {$body = $request->getContent(); // body mentah$ts = (string) $request->header('X-WaroengPay-Timestamp');$expected = hash_hmac('sha256', $ts . '.' . $body, config('services.waroengpay.webhook_secret'));if (! hash_equals($expected, (string) $request->header('X-Signature')) || abs(time() - (int) $ts) > 300) {abort(401);}$event = json_decode($body, true);$testMode = str_starts_with((string) config('services.waroengpay.key'), 'wp_test_');if (($event['livemode'] ?? true) === false && ! $testMode) {return response()->noContent(); // event mode uji}if ($event['event'] === 'payment.paid') {$order = Order::where('waroengpay_id', $event['data']['id'])->first();if ($order && ! $order->paid_at && (int) $order->total === $event['data']['base_amount']) {$order->update(['paid_at' => now()]);}}return response()->noContent();});
Webhook dikirim tanpa token CSRF, jadi kecualikan alamatnya dari pemeriksaan CSRF. Di Laravel 11 ke atas caranya lewat bootstrap/app.php seperti di bawah. Di Laravel 10 ke bawah, tambahkan alamat yang sama ke $except di app/Http/Middleware/VerifyCsrfToken.php.
->withMiddleware(function (Middleware $middleware) {$middleware->validateCsrfTokens(except: ['webhook/waroengpay',]);})
8. Sebelum go live
- Ganti
WAROENGPAY_API_KEYke key live (wp_live_...) danWAROENGPAY_WEBHOOK_SECRETke secret webhook live. - Isi URL webhook default di dashboard, lalu tekan Kirim tes dan pastikan servermu membalas dengan status 2xx.
- Pastikan pesanan hanya lunas lewat webhook atau pengecekan status, dan nominalnya selalu dicocokkan.
- Tangani
payment.paiddengandata.late = true, yaitu pembayaran setelah tagihan kedaluwarsa yang dicocokkan admin. Event ini bisa datang setelahpayment.expired.
Error yang sering muncul
| Kode | Artinya | Yang perlu dilakukan |
|---|---|---|
401 invalid_api_key | API key salah atau sudah dibuat ulang | Periksa variabel lingkungan, buat key baru di dashboard bila perlu |
400 invalid_amount | Nominal bukan bilangan bulat Rp1.000 sampai Rp10.000.000 | Kirim angka rupiah tanpa titik ribuan, misalnya 50000 |
403 account_not_active | Akun belum disetujui atau sedang nonaktif | Tunggu persetujuan admin atau hubungi kami |
429 rate_limited | Terlalu banyak request, misalnya lebih dari 30 tagihan per menit | Tunggu sesuai header Retry-After |
503 amount_busy | Kode unik untuk nominal itu sedang habis | Coba lagi beberapa saat kemudian |
Referensi lengkap semua endpoint ada di dokumentasi API. Kalau kamu berjualan lewat toko online, lihat juga solusi untuk toko online.