← Kembali ke Blog

Integrasi QRIS Pakasir ke PHP dari Nol Dan Bug Timezone

20 April 2026

Waktu gue lagi bangun Toko Topup — platform jual produk digital berbasis PHP/MySQL — gue butuh payment gateway yang support QRIS dan gampang diintegrasikan tanpa harus punya rekening bisnis dulu.

Pilihan jatuh ke Pakasir. Dokumentasinya minimalis, komunitas yang nulis tentang ini hampir nol, jadi gue tulis sendiri pengalaman integrasi ini dari awal sampai production-ready — termasuk bug timezone yang cukup menyebalkan.

Apa itu Pakasir?

Pakasir adalah payment gateway lokal Indonesia yang support QRIS. Cocok untuk developer indie atau UMKM yang butuh solusi pembayaran digital tanpa proses verifikasi yang ribet.

Flow Pembayaran

Sebelum nulis kode, penting untuk paham alurnya dulu:

User checkout -> PHP kirim request ke Pakasir API -> Pakasir return QR code + expired_at -> User scan QR via mobile banking -> Pakasir update status transaksi -> PHP polling untuk cek status -> Order dikonfirmasi otomatis

Prerequisites

Step 1 — Buat Transaksi QRIS

Endpoint yang dipakai: POST https://app.pakasir.com/api/transactioncreate/qris

Payload yang dikirim:

$payload = [
    "project"  => $slug_proyek,
    "order_id" => $order_id,
    "amount"   => $amount,
    "api_key"  => $api_key
];

Kirim Request Ke pakasir

$ch = curl_init('https://app.pakasir.com/api/transactioncreate/qris');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
    CURLOPT_POSTFIELDS     => json_encode($payload),
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_SSL_VERIFYPEER => false
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($httpCode !== 200) {
    throw new RuntimeException("Pakasir API error. HTTP Code: $httpCode");
}

$result = json_decode($response, true);

Kenapa curl_setopt_array?

Lebih clean daripada manggil curl_setopt satu per satu. Semua opsi terdefinisi dalam satu blok yang mudah dibaca.

Kenapa CURLOPT_SSL_VERIFYPEER => false?

Untuk menghindari SSL verification error di environment lokal atau shared hosting tertentu. Di production sebaiknya di-set true dan pastikan sertifikat CA server up to date.

Step 2 — Ambil Data dari Response

Setelah request berhasil, Pakasir return JSON dengan struktur seperti ini:

{
  "payment": {
    "qr_string": "THIS.IS.JUST.AN.EXAMPLE.FOR.SANDBOX.00020101021226610016ID.CO.SHOPEE.WWW01189360091800216005230208216005230303UME51440014ID.CO.QRIS.WWW.1111",
    "expired_at": "2026-04-20T02:30:00.1234567890Z"
  }
}

Akses field-nya:

$qr_string    = $result['payment']['qr_string'];
$expired_at = $result['payment']['expired_at'];

Step 3 — Bug Timezone

Ini bagian yang paling penting dan paling jarang dibahas. expired_at dari Pakasir formatnya seperti ini:

2026-04-20T02:30:00.1234567890Z

Ada dua masalah di sini:

Masalah 1 — Microsecond terlalu panjang PHP DateTime hanya support 6 digit microsecond. Pakasir kadang return lebih dari 6 digit, yang bikin parsing gagal atau hasilnya salah.

Masalah 2 — Timezone UTC, bukan WIB expired_at dalam UTC. Kalau langsung ditampilkan ke user tanpa konversi, jam expiry-nya akan kelihatan 7 jam lebih awal dari seharusnya.

Solusinya:

$rawExpiredAt   = $result['payment']['expired_at'];
$cleanExpiredAt = preg_replace('/(\.\d{6})\d+/', '$1', $rawExpiredAt);
$expiredDt      = new DateTime($cleanExpiredAt, new DateTimeZone('UTC'));
$expiredDt->setTimezone(new DateTimeZone('Asia/Jakarta'));
$expired        = $expiredDt->format('Y-m-d H:i:s');

Breakdown tiap baris:

Step 4 — Simpan ke Database

$stmt = $pdo->prepare("
    INSERT INTO orders (order_id, amount, qr_string, expired_at, status)
    VALUES (:order_id, :amount, :qr_string, :expired_at, 'pending')
");

$stmt->execute([
    ':order_id'   => $order_id,
    ':amount'     => $amount,
    ':qr_url'     => $qr_url,
    ':expired_at' => $expired
]);

Penting: simpan expired_at dalam WIB yang sudah dikonversi, bukan raw string dari Pakasir. Ini menghindari timezone confusion saat query perbandingan waktu di MySQL.

Step 5 — Tampilkan ke user

date_default_timezone_set('Asia/Jakarta');

$expired_ts       = strtotime($trx['expired_at']);
$expired_time_wib = date('H:i', $expired_ts) . ' WIB';
$total_format     = 'Rp ' . number_format($trx['amount'], 0, ',', '.');

Hasilnya: jam expiry yang akurat sesuai timezone user di Indonesia.

Kesimpulan

Integrasi Pakasir ke PHP sebenarnya straightforward — hanya 4 field di payload dan satu endpoint. Yang bikin tricky adalah handling expired_at-nya karena:

  1. Format microsecond yang tidak standard
  2. Response dalam UTC sementara user kita di WIB

Dua baris preg_replace + setTimezone itu yang menyelamatkan logika expiry di seluruh sistem.

Referensi

Pakasir, PHP DateTime Documentation, PHP DateTimeZone