Implementasi QRIS Webhook yang Benar. Menerima Notifikasi Pembayaran, Retry Logic, dan Idempotency

Implementasi QRIS Webhook yang Benar: Panduan Lengkap untuk Notifikasi Pembayaran yang Andal
Transaksi QRIS di Indonesia melesat 226,54% dalam setahun terakhir, dengan 50,50 juta pengguna dan 32,71 juta merchant, menunjukkan pentingnya sistem pembayaran yang andal. Pertumbuhan ini membawa tantangan baru: bagaimana memastikan notifikasi pembayaran sampai ke sistem Anda dengan tepat waktu dan tanpa duplikasi?
Artikel ini membahas cara implementasi webhook QRIS yang benar, dari menerima notifikasi pembayaran hingga menerapkan retry logic dan idempotency untuk mencegah pemrosesan transaksi ganda.
Mengapa Webhook QRIS Sering Gagal
Salah satu masalah umum yang dihadapi merchant QRIS adalah koneksi internet buruk yang menyebabkan notifikasi pembayaran tidak terkirim, menyebabkan penjual salah serahkan barang atau menunggu notifikasi yang tidak pernah datang. Masalah ini bukan berasal dari sisi QRIS atau payment gateway, tetapi dari kurangnya persiapan di sisi merchant.
Svix's 2024 webhook reliability research menunjukkan rata-rata webhook consumer mengalami 3,5% failure rate, dan 15% implementasi webhook tidak memiliki mekanisme retry yang benar, menyebabkan potensi kehilangan pesanan. Angka ini cukup besar untuk bisnis yang bergantung pada pembayaran online.
Payment gateway seperti Midtrans dan Xendit secara otomatis melakukan retry webhook jika notifikasi gagal terkirim, yang berarti merchant harus memastikan handler webhook mereka idempotent untuk menghindari pemrosesan duplikat. Tanpa persiapan yang matang, sistem Anda bisa menerima notifikasi yang sama berkali-kali.
Struktur Dasar Webhook Handler
Handler webhook yang baik harus memenuhi tiga prinsip utama: respons cepat, pemrosesan asynchronous, dan idempotency. Berikut contoh struktur dasar dengan Node.js/Express:
app.post('/webhook/qris', async (req, res) => {
// 1. Validasi signature
const signature = req.headers['x-payment-signature'];
if (!verifySignature(req.body, signature)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 2. Return 200 OK immediately (before processing)
res.status(200).json({ received: true });
// 3. Process asynchronously
processPaymentNotification(req.body).catch(err => {
console.error('Webhook processing failed:', err);
// Implement retry logic here
});
});Kunci di sini adalah mengembalikan respons 200 OK sebelum memproses transaksi. Ini memberi tahu payment gateway bahwa notifikasi diterima, sementara pemrosesan berjalan di background. Jika server Anda crash setelah mengirim 200 tapi sebelum pemrosesan selesai, Anda memerlukan mekanisme rekonkiasi.
Implementasi Idempotency dengan Benar
Idempotency berarti operasi yang sama dilakukan berkali-kali menghasilkan efek yang sama seperti dilakukan sekali. Untuk webhook QRIS, ini berarti jika notifikasi pembayaran yang sama diterima dua kali, sistem hanya akan memprosesnya sekali.
Pola umum adalah menggunakan ID transaksi dari payment gateway sebagai kunci idempotency:
async function processPaymentNotification(payload) {
const { transaction_id, status, amount } = payload;
// Check if already processed
const existing = await db.transactions.findOne({
payment_gateway_id: transaction_id
});
if (existing) {
// Already processed - verify no change in critical data
if (existing.status !== status) {
// Status changed legitimately (e.g., pending -> settled)
await updateTransactionStatus(existing, status);
}
return;
}
// Process new transaction
const transaction = await createTransaction(transaction_id, amount, status);
await fulfillOrder(transaction);
}Sampai Semester I 2025, QRIS telah menjangkau 57 juta pengguna dan 39,3 juta merchant yang 93,16% di antaranya adalah UMKM, menegaskan dominasi UMKM dalam adopsi QRIS. Untuk UMKM yang baru mulai mengimplementasikan sistem pembayaran digital, pemahaman tentang idempotency ini sangat penting untuk mencegah kebingungan dan kerugian.
Retry Logic yang Tepat
Ketika pemrosesan webhook gagal (database down, API error third-party, dll), Anda tidak bisa mengandalkan payment gateway untuk terus mengirim retry selamanya. Midtrans dan Xendit memiliki batas retry mereka sendiri. Setelah itu, notifikasi berhapat datang dan order Anda tergantung.
Implementasikan queue system untuk retry logic:
async function processPaymentNotification(payload) {
try {
await processWithDb(payload);
} catch (error) {
// Add to retry queue with exponential backoff
await retryQueue.add('process-payment', {
payload,
attempt: 1
}, {
attempts: 5,
backoff: {
type: 'exponential',
delay: 2000 // 2s, 4s, 8s, 16s, 32s
}
});
}
}
// Queue processor
retryQueue.process(async (job) => {
const { payload, attempt } = job.data;
try {
await processWithDb(payload);
} catch (error) {
if (attempt >= 5) {
// Max retries reached - alert human
await alertTeam(payload, error);
throw error; // Mark as failed
}
throw error; // Trigger next retry
}
});Pola ini memberikan beberapa keuntungan: pemrosesan ulang otomatis dengan exponential backoff (menghindari thundering herd), batas maksimal retry sebelum eskalasi ke manusia, dan decoupling dari webhook endpoint yang tetap responsif.
Rekonsiliasi: Safety Net Terakhir
Meski dengan webhook yang andal, Anda tetap memerlukan proses rekonsiliasi berkala untuk menangkap edge case yang lolos: notifikasi yang hilang selamanya, bug di idempotency logic, atau kegagalan sistem yang parah. Ini mencakup pembayaran yang berhasil di sisi payment gateway tetapi tidak tercatat di sistem Anda.
// Run every hour
async function reconcileTransactions() {
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
// Fetch settled transactions from payment gateway
const gatewayTransactions = await paymentGateway.settlements.list({
created_after: yesterday
});
for (const gwTx of gatewayTransactions) {
const local = await db.transactions.findOne({
payment_gateway_id: gwTx.id
});
if (!local && gwTx.status === 'settled') {
// Found settled payment not in our system
console.warn('Unreconciled transaction:', gwTx.id);
await createTransactionFromGateway(gwTx);
await alertTeam('Reconciled transaction', gwTx.id);
}
}
}Rekonsiliasi ini bukan pengganti webhook yang andal, tetapi pelengkap. Ia menangkap apa yang lolos dari net pertahanan utama Anda. Untuk sistem produksi, jalankan ini sebagai cron job setiap 1-6 jam tergantung volume transaksi.
Testing Webhook di Development
Salah satu tantangan terbesar dalam mengembangkan webhook adalah bagaimana testing di environment lokal. Payment gateway tidak bisa mengirim request ke localhost Anda. Gunakan salah satu pendekatan ini:
Ngrok atau similar - Tunnel public ke localhost Anda. Praktis untuk development tapi tidak ideal untuk production.
Staging environment - Deploy ke staging server yang bisa diakses publik. Lebih dekat dengan production.
Mocking payment gateway - Simulasi webhook request secara manual. Baik untuk unit testing tapi tidak menangkap integrasi yang sebenarnya.
Pendekatan yang sehat adalah kombinasi: mocking untuk unit test, staging untuk integration test, dan ngrok hanya untuk debugging sesi development.
Langkah Selanjutnya
Implementasi webhook yang benar adalah fondasi dari sistem pembayaran digital yang andal. Dengan pertumbuhan QRIS yang terus melonjak, bisnis yang memiliki infrastruktur pembayaran yang kuat akan memiliki keunggulan kompetitif. Tetapi webhook hanya satu bagian dari puzzle integrasi pembayaran yang lebih besar. Untuk panduan lengkap tentang integrasi payment gateway dan QRIS untuk bisnis Anda, baca panduan lengkap integrasi payment gateway dan QRIS yang membahas seluruh ekosistem pembayaran digital.
Jika bisnis Anda baru memulai transformasi digital dan memerlukan bimbingan tentang infrastruktur teknis yang tepat, panduan transformasi digital UMKM memberikan roadmap praktis dari sistem manual ke digital. Atau jika Anda siap untuk mengimplementasikan sistem pembayaran yang andal tetapi tidak yakin dengan arsitektur yang tepat untuk bisnis Anda, konsultasikan dengan tim kami untuk mendapatkan solusi yang dirancang khusus untuk kebutuhan Anda.

