Webhook pembayaran QRIS adalah pesan otomatis yang dikirim server penyedia QRIS ke server kamu setiap kali status pembayaran berubah, jadi sistem toko tidak perlu terus bertanya. Di TemanQRIS, webhook dikirim sebagai HTTP POST bertanda tangan HMAC-SHA256 untuk event payment.awaiting_confirmation dan payment.confirmed, dan dicoba ulang sampai 3 kali kalau server kamu gagal membalas.
Webhook, polling, dan callback URL: apa bedanya
Tiga istilah ini sering tertukar, padahal fungsinya beda jauh. Kalau salah pilih, toko bisa mengirim barang untuk pesanan yang belum dibayar.
| Cara | Siapa yang memulai | Bisa jadi bukti bayar? | Catatan |
|---|---|---|---|
| Webhook | Server TemanQRIS mengirim ke server kamu | Ya, kalau tanda tangan valid | Real time, server kamu cukup menunggu |
Polling (GET /api/qris/orders/:orderId) | Server kamu bertanya berkala | Ya, karena datanya dari API dengan API key | Ada jeda antara pembayaran dan saat kamu bertanya |
| Callback URL | Browser pembeli diarahkan ke halaman kamu | Tidak | Hanya untuk pengalaman pengguna, siapa pun bisa membuka URL-nya |
Pola yang kami sarankan: webhook sebagai sumber utama, polling ringan sebagai cadangan, dan callback URL murni untuk menampilkan halaman terima kasih.
Event webhook pembayaran QRIS di TemanQRIS
Ada dua event, dan keduanya dikirim ke webhook_url yang kamu isi saat membuat pembayaran.
payment.awaiting_confirmation: pembeli menekan tombol "Sudah Bayar" di halaman pembayaran atau widget. Ini pemberitahuan dari pembeli, belum bukti dana masuk.payment.confirmed: pesanan sudah berstatuspaid. Di mode reguler, ini terjadi setelah kamu mengonfirmasi lewat dashboard atauPOST /api/qris/orders/:orderId/verify. Di mode Automatic Payment Gateway, server TemanQRIS sendiri yang mencocokkan pembayaran lalu mengirim event ini.
Jadi, seberapa "otomatis" cek status bayarnya tergantung mode. Di mode reguler, uang masuk langsung ke rekening QRIS kamu dan TemanQRIS tidak bisa melihat mutasinya, sehingga konfirmasi akhir tetap dari merchant. Yang otomatis adalah penyampaian statusnya ke sistem kamu. Di mode gateway (butuh KYC, maksimal Rp1.000.000 per transaksi), konfirmasinya juga otomatis.
Isi payload dan header webhook
Webhook dikirim sebagai POST dengan body JSON. Contoh untuk pesanan yang sudah lunas:
{
"event": "payment.confirmed",
"idempotency_key": "6f1c2a9e-3b7d-4e55-9a10-2c8f0d4b7e21",
"timestamp": "2026-10-17T08:15:02.000Z",
"data": {
"order_id": "INV-20261017-0042",
"link_code": "AB12CD34EF56",
"amount": 125000,
"description": "Pesanan INV-20261017-0042",
"status": "paid",
"paid_at": "2026-10-17T08:14:58.000Z",
"created_at": "2026-10-17T07:58:11.000Z"
}
}
Untuk pembayaran mode gateway, objek data berisi field tambahan: payment_mode, payment_attempt_id, base_amount, fee_amount, unique_code, total_amount, provider_fee_amount, net_amount, dan confirmed_by. Nilai net_amount adalah jumlah yang masuk ke saldo kamu, yaitu base_amount + fee_amount - provider_fee_amount.
Header yang ikut dikirim:
X-TemanQRIS-Signature: tanda tangan HMAC-SHA256 atas raw body, formatnyasha256=<hex>.X-TemanQRIS-Event: nama event.X-TemanQRIS-Idempotency-Key: kunci unik pengiriman, sama denganidempotency_keydi body.X-TemanQRIS-Retry: hanya ada di pengiriman ulang, berisi nomor percobaan ulang.
Cara menyiapkan webhook pembayaran QRIS
- Buat webhook secret di dashboard. Tanpa secret, webhook tidak dikirim sama sekali. Secret berupa 64 karakter heksadesimal, simpan di environment variable server kamu.
- Siapkan endpoint publik HTTPS. URL yang mengarah ke localhost, IP privat, atau link-local ditolak. Di production wajib HTTPS.
- Kirim
webhook_urlper pembayaran. Isi di bodyPOST /api/qris/payment-link,POST /api/qris/generate, atauPOST /api/payment-gateway/payments. Kalau pakai widget, gunakan atributdata-webhook. - Verifikasi tanda tangan dari raw body. Hitung HMAC-SHA256 atas teks body persis seperti yang diterima, lalu bandingkan dengan perbandingan waktu-konstan.
- Cegah proses ganda. Simpan
idempotency_keyyang sudah diproses dan abaikan kalau datang lagi. - Balas cepat dengan status 2xx. Batas waktu tunggu 10 detik. Simpan event, balas, lalu proses pekerjaan berat di background.
Contoh penerima webhook di Node.js:
const crypto = require('crypto');
const express = require('express');
const app = express();
app.post('/webhooks/temanqris', express.raw({ type: 'application/json' }), async (req, res) => {
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.TEMANQRIS_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const got = req.get('X-TemanQRIS-Signature') || '';
if (got.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected))) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString('utf8'));
if (await sudahDiproses(event.idempotency_key)) return res.status(200).end();
await simpanEvent(event); // proses lanjutan di worker/background
res.status(204).end();
});
Versi PHP dengan hash_hmac dan hash_equals, plus alur lengkap dari checkout sampai cek status, ada di panduan integrasi API QRIS di website.
Retry: apa yang terjadi kalau server kamu sedang down
Kalau server kamu membalas selain 2xx, timeout, atau tidak bisa dihubungi, TemanQRIS mencoba lagi sampai 3 kali dengan jeda yang makin panjang: sekitar 30 detik, 2 menit, lalu 10 menit setelah kegagalan sebelumnya. Total ada 4 percobaan. Setiap pengiriman ulang memakai payload dan idempotency_key yang sama, makanya deduplikasi di langkah 5 itu wajib.
Dua hal kecil yang sering bikin webhook dianggap gagal:
- Redirect. Pengirim tidak mengikuti redirect, jadi balasan 301 atau 302 dihitung gagal. Ini sering terjadi kalau URL tanpa garis miring di akhir diarahkan ke versi dengan garis miring, atau HTTP diarahkan ke HTTPS.
- Firewall atau proteksi bot. Kalau endpoint kamu ada di belakang proteksi yang menantang request tanpa browser, webhook tidak akan lolos. Kecualikan path webhook dari aturan itu dan andalkan verifikasi tanda tangan.
Cara cek status bayar kalau webhook belum masuk
Webhook bisa terlambat atau gagal, jadi siapkan jalur cadangan dengan GET /api/qris/orders/:orderId memakai header X-API-Key. Respons-nya memuat status, is_paid, is_expired, paid_at, dan expires_at.
Cek status order tidak memotong kuota harian (kuota 50 request di Free dan 1.000 di Premium dihitung dari endpoint yang membuat atau mengubah data, seperti generate, payment link, dan verify). Meski begitu, polling tetap sebaiknya hemat, demi server kamu sendiri dan supaya log tidak penuh. Yang kami sarankan:
- Polling hanya saat pembeli masih membuka halaman menunggu, misalnya setiap 20 sampai 30 detik.
- Berhenti begitu status
paid,expired, ataucancelled. - Jalankan job rekonsiliasi harian untuk pesanan yang masih
awaiting_confirmation, supaya tidak ada yang tertinggal.
Referensi lengkap endpoint dan contoh respons ada di dokumentasi API QRIS.
Cara menguji webhook tanpa transaksi sungguhan
Kabar baiknya, kamu bisa menguji kedua event tanpa memindahkan uang sungguhan. Caranya:
- Jalankan endpoint webhook di komputer kamu, lalu buka akses publiknya lewat tunnel seperti ngrok atau Cloudflare Tunnel. Pakai URL HTTPS yang diberikan tunnel sebagai
webhook_url. - Buat link pembayaran dengan nominal kecil dan Order ID khusus uji coba, misalnya
TES-WEBHOOK-01. - Buka link tersebut di browser dan klik "Sudah Bayar". Server kamu akan menerima
payment.awaiting_confirmation. - Tandai order itu lunas lewat dashboard atau endpoint verify. Server kamu akan menerima
payment.confirmed. - Matikan endpoint sebentar lalu ulangi dengan order baru untuk melihat perilaku retry dan memastikan deduplikasi berjalan.
Ingat, membuat link dan verify ikut memotong kuota harian, jadi rencanakan ujinya. Setelah selesai, batalkan order uji coba supaya laporan tidak tercampur. Panduan langkah per langkah dengan tangkapan layar dashboard ada di tutorial TemanQRIS, dan kalau kamu belum punya alur pembayaran sama sekali, mulai dari cara terima pembayaran QRIS untuk toko online kecil.
Checklist sebelum go-live
- Webhook secret sudah dibuat dan tersimpan di server, bukan di kode.
- Endpoint HTTPS bisa diakses publik dan tidak redirect.
- Tanda tangan diverifikasi dari raw body dengan perbandingan waktu-konstan.
idempotency_keydisimpan dan dicek sebelum memproses.- Pesanan hanya dianggap lunas dari
payment.confirmedatau statuspaiddari API, bukan dari callback URL. - Ada polling cadangan dengan jeda wajar dan job rekonsiliasi harian.
- Log mencatat event, order_id, dan hasil verifikasi, tanpa menyimpan secret.
Pertanyaan yang Sering Diajukan (FAQ)
Apa beda webhook dan callback URL?
Webhook dikirim dari server TemanQRIS ke server kamu dan bisa diverifikasi lewat tanda tangan HMAC. Callback URL hanya mengarahkan browser pembeli ke halaman kamu, jadi tidak boleh dipakai sebagai bukti pembayaran.
Apakah webhook TemanQRIS mengonfirmasi pembayaran secara otomatis?
Tergantung mode. Di mode reguler, event payment.confirmed dikirim setelah merchant mengonfirmasi lunas. Di mode Automatic Payment Gateway, server TemanQRIS mencocokkan pembayaran sendiri lalu mengirim event tersebut.
Berapa kali webhook dicoba ulang kalau gagal?
Sampai 3 kali setelah percobaan pertama, dengan jeda sekitar 30 detik, 2 menit, dan 10 menit. Semua pengiriman ulang membawa idempotency_key yang sama.
Kenapa webhook pembayaran QRIS tidak terkirim?
Biasanya karena webhook secret belum dibuat di dashboard, URL mengarah ke alamat privat atau tidak HTTPS, endpoint membalas redirect, atau proteksi bot memblokir request dari server.