FQrisDocs
Panduan

Top-up koin

Dari memilih paket di aplikasi Android sampai koin bertambah tepat satu kali, lewat backend-mu.

7 menit bacaDiperbarui 29 Okt 2026

Alur end-to-end

Aplikasi Android tidak memanggil FQris langsung. Backend-mu yang membuat payment, menerima webhook, lalu menambah koin. Aplikasi hanya menampilkan checkout dan membaca saldo dari backend-mu.

  1. 1
    Pembeli memilih paket Aplikasi meminta top-up ke backend-mu, misalnya paket 200 koin seharga Rp20.000.
  2. 2
    Backend membuat payment Panggil POST /v1/payments dengan amount, reference_id berisi ID order, dan metadata berisi user_id. Simpan order dengan status awaiting_payment.
  3. 3
    Pembeli membayar Aplikasi membuka checkout_url, atau merender qris.string sendiri. Pembeli membayar lewat e-wallet atau m-banking.
  4. 4
    FQris mengirim webhook Listener mendeteksi uang masuk, payment menjadi paid, dan webhook payment.paid dikirim ke backend-mu.
  5. 5
    Backend mengkredit koin Backend memverifikasi webhook, mengambil ulang payment, lalu menambah koin sekali. Aplikasi menerima kabar "Koin bertambah" lewat push, SSE, atau polling ke backend-mu.

Buat payment dari backend

Isi amount dengan harga paket dalam rupiah bulat dan reference_id dengan ID order, misalnya TOPUP-88812. Kirim Idempotency-Key yang sama agar retry setelah timeout tidak membuat payment ganda. metadata ikut kembali di payment dan webhook, jadi user_id tersedia saat mengkredit. Simpan id payment bersama order.

buat-payment
1import FQris from '@fqris/node';
2const fqris = new FQris(process.env.FQRIS_KEY);
3
4const payment = await fqris.payments.create({
5 amount: 20000,
6 reference_id: 'TOPUP-88812',
7});
8console.log(payment.checkout_url);

Kalau FQris menjawab 503 (NO_HEALTHY_SOURCE atau MAINTENANCE), tampilkan pesan "coba lagi" ke pembeli dan ulangi nanti.

API key hanya di backend
Jangan menaruh fq_live_ di aplikasi Android, termasuk di BuildConfig atau remote config. APK mudah dibongkar, dan pemegang key bisa membuat payment atas namamu.

Tampilkan checkout

Pembeli harus membayar final_amount persis, yaitu amount ditambah kode unik acak (rentang bawaan 1 sampai 499). Rp20.000 bisa menjadi Rp20.137. Tampilkan final_amount, bukan amount.

Buka checkout_url di Custom Tab atau WebView. Halaman itu menampilkan QR dan hitung mundur, lalu berganti ke layar sukses begitu pembayaran terdeteksi.

Bayar dari HP yang sama
Pembeli tidak bisa memindai layar HP-nya sendiri. Di halaman checkout ia menekan Simpan QR, lalu di aplikasi pembayarannya memilih Scan dan gambar dari galeri.

Kalau kamu merender qris.string sendiri, hitung mundur dari expires_at dan pantau status lewat backend-mu, bukan langsung ke FQris.

Kredit koin dari webhook

Kredit koin hanya dari webhook payment.paid yang terverifikasi. Jangan mengkredit dari return_url atau dari klien Android: keduanya bisa dipalsukan.

  1. 1
    Verifikasi signature Pakai raw body dan toleransi 5 menit, seperti di panduan Webhook.
  2. 2
    Ambil ulang payment Panggil GET /v1/payments/{id}, pastikan status bernilai paid, lalu cocokkan reference_id dan nominal dengan order.
  3. 3
    Kredit di dalam transaksi DB Beri kunci unik payment_id pada catatan kredit, dan satu kunci unik per order (lihat Kasus khusus). Event yang datang dua kali tidak akan mengkredit dua kali.
  4. 4
    Balas 2xx Balas dalam 10 detik dan kerjakan pekerjaan berat setelahnya.
webhook-handler
1app.post('/webhooks/fqris', express.raw({ type: '*/*' }), async (req, res) => {
2 const sig = req.headers['fqris-signature'];
3 // 1. Verifikasi dengan raw body + toleransi 5 menit
4 const event = fqris.webhooks.constructEvent(req.body, sig, process.env.WHSEC);
5 // 2. Ambil ulang status sebelum mengkredit
6 const p = await fqris.payments.retrieveAndAssertPaid(event.data.object.id);
7 await creditCoinsOnce(p.id, p.metadata.user_id);
8 res.sendStatus(200);
9});

Kasus khusus

  1. 1
    paid_late Pembeli membayar setelah expires_at, masih dalam masa tenggang (bawaan 30 menit). payment.paid datang dengan paid_late: true. Ordermu mungkin sudah kamu tandai gagal: kredit atau refund sesuai kebijakanmu.
  2. 2
    payment.reversed Konfirmasi manual dibatalkan Owner dalam 24 jam. Tarik kembali koin yang sudah dikredit.
  3. 3
    payment.overpaid Pembeli membayar dua kali QR yang sama. Status payment tidak berubah otomatis: putuskan sendiri apakah kelebihannya dikredit atau di-refund.
  4. 4
    paid_after_cancel Kamu membatalkan payment, tetapi pembeli membayar QR-nya selama masa tenggang. Payment menjadi paid dengan paid_after_cancel: true dan payment.paid terkirim, sama seperti paid_late: kredit atau refund sesuai kebijakanmu.
  5. 5
    expired atau cancelled Tandai order gagal hanya setelah masa tenggang habis. Sebelum itu payment expired atau cancelled masih bisa menjadi paid, dan payment.paid yang datang kemudian tetap kamu proses (kredit atau refund sesuai kebijakan).
  6. 6
    Satu order, satu kredit Untuk mencoba lagi, buat payment baru dengan reference_id baru: satu reference_id hanya untuk satu payment, apa pun statusnya (409 DUPLICATE_REFERENCE). Payment lama dan baru bisa sama-sama lunas, jadi beri kunci kredit unik per order di DB, bukan hanya per payment_id. Payment kedua yang lunas untuk order yang sudah dikredit kamu refund lewat POST /v1/payments/{id}/refunds. Kalau kamu hanya membatalkan payment lama sebelum membuat yang baru, itu tidak cukup, karena payment cancelled pun masih bisa menjadi paid selama masa tenggang.

Uji sebelum live

  1. 1
    Pakai key test Buat payment dengan fq_test_ dan daftarkan endpoint webhook mode test. Data test tidak pernah tercampur dengan live.
  2. 2
    Simulasikan bayar Panggil POST /v1/sandbox/payments/{id}/simulate dengan {"result":"paid"}. Hasil lain: expired dan paid_late. Ulangi sampai koin bertambah tepat satu kali.
  3. 3
    Satu top-up uang nyata Ganti ke key dan endpoint live, lalu lakukan satu top-up kecil dari aplikasi pembayar lain, bukan dari akun DANA Bisnis penerima.
Apakah halaman ini membantu?