Integrasi Xendit QRIS di Aplikasi Next.js Menggunakan Webhook
Panduan lengkap untuk menerapkan pembayaran QRIS menggunakan endpoint terbaru /payment_requests dari Xendit dan webhook di project Next.js App Router modern.
Integrasi Xendit QRIS di Aplikasi Next.js Menggunakan Webhook
Dalam transaksi digital modern, QRIS sudah menjadi cara paling seamless untuk menerima pembayaran dari e-wallet atau aplikasi mobile banking apa pun di Indonesia.
Saat membangun web app food delivery dengan Next.js App Router, saya butuh cara yang bersih, andal, dan scalable untuk menangani pembayaran QRIS —
dan saya memilih endpoint /payment_requests dari Xendit dengan webhook handling sebagai strategi utamanya.
Berikut cara saya mengimplementasikannya langkah demi langkah.
🔧 Step 1: Buat Payment Request via QRIS
Saya membuat custom API route di project Next.js saya untuk meneruskan data dari frontend ke endpoint terbaru /payment_requests milik Xendit.
// /api/xendit/create-payment-request/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function POST(req: NextRequest) {
const body = await req.json()
const payload = {
reference_id: `cbk_${Date.now()}`,
currency: 'IDR',
amount: body.amount,
payment_method: {
type: 'QRIS',
},
metadata: {
order_id: body.orderId,
},
}
const res = await fetch('https://api.xendit.co/payment_requests', {
method: 'POST',
headers: {
Authorization: `Basic ${Buffer.from(process.env.XENDIT_API_KEY + ':').toString('base64')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
const data = await res.json()
return NextResponse.json(data)
}
Respons dari Xendit berisi:
id: payment request IDstatus:PENDINGactions.qr_checkout_string: string untuk menghasilkan QRexpires_at: waktu QRIS kedaluwarsa
🖼️ Step 2: Render QR Code di Modal
Setelah frontend menerima qr_checkout_string, saya generate QR code menggunakan react-qr-code:
import QRCode from 'react-qr-code'
export default function InvoicePopup({ qrString }: { qrString: string }) {
return (
<div className="flex flex-col items-center p-6">
<QRCode value={qrString} size={200} />
<p className="text-sm text-center mt-2 text-gray-500">
Scan with any QRIS-compatible app to complete your payment.
</p>
</div>
)
}
Tanpa redirect, tanpa friksi — pengalaman tetap native.
📬 Step 3: Tangani Webhook dari Xendit (Sumber Kebenaran di Server)
Daripada polling, saya mengimplementasikan secure webhook receiver di:
POST /api/xendit/webhook
Endpoint ini mendengarkan event dari Xendit seperti payment_request.succeeded dan payment_request.expired.
// /api/xendit/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/firebase'
export async function POST(req: NextRequest) {
const callbackToken = req.headers.get('x-callback-token')
if (callbackToken !== process.env.XENDIT_CALLBACK_TOKEN) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const event = await req.json()
const { status, metadata, id } = event
// Update Firestore order by metadata.order_id
await db.collection('orders').doc(metadata.order_id).update({
status: status === 'SUCCEEDED' ? 'paid' : status.toLowerCase(),
payment_id: id,
updated_at: Date.now(),
})
return NextResponse.json({ success: true })
}
🔐 Selalu validasi x-callback-token untuk melindungi dari request webhook palsu.
✅ Step 4: Perbarui UI dari Firestore
Karena saya menggunakan Firestore untuk data order, saya cukup mendengarkan status order di sisi client. Ketika webhook memperbarui order menjadi paid, frontend merefleksikan perubahan secara real-time.
useEffect(() => {
const unsub = onSnapshot(doc(db, 'orders', orderId), (doc) => {
if (doc.data()?.status === 'paid') {
router.push('/checkout/success')
}
})
return () => unsub()
}, [orderId])
🧠 Kenapa Webhook Dibanding Polling?
Polling
- Berbasis frontend, cek berkala
- Bisa miss timing atau overload API
- Cocok untuk feedback UI sementara
Webhook
- Berbasis backend, push real-time
- Selalu akurat, event-driven
- Paling tepat untuk logic production
✨ Penutup
Integrasi QRIS dengan Xendit di aplikasi Next.js sangat powerful jika diterapkan dengan benar.
Dengan mengandalkan webhook sebagai sumber kebenaran dan Firestore untuk UI yang reaktif, saya membangun pengalaman checkout yang mulus dan scalable untuk kondisi nyata — dari mobile sampai desktop.
Hasilnya: proses pembayaran yang cepat, terasa native, dan benar-benar bekerja — tanpa SDK yang berat atau redirect ke layanan eksternal.
