Webhook pembayaran: cara kerja dan cara memverifikasinya dengan aman
Apa itu webhook payment gateway, kenapa wajib diverifikasi HMAC, dan cara memprosesnya dengan aman: body mentah, timestamp, idempotensi, dan pengiriman ulang.
Tim WaroengPay · Diperbarui · 8 menit baca
Webhook adalah cara payment gateway memberi tahu servermu bahwa sesuatu terjadi, misalnya tagihan sudah lunas. Servermu tidak perlu bertanya berulang kali "sudah dibayar belum?"; payment gateway yang mengirim kabar lewat request HTTP POST ke alamat yang kamu tentukan, begitu statusnya berubah.
Karena webhook bisa memicu pengiriman barang atau produk digital, alamat webhook-mu perlu dijaga seperti pintu kasir: siapa pun bisa mengetuk, jadi setiap kiriman wajib diperiksa keasliannya.
Webhook, polling, atau halaman sukses?
| Cara | Kelebihan | Kekurangan |
|---|---|---|
| Webhook | Kabar datang seketika tanpa request berulang | Butuh alamat publik dan verifikasi tanda tangan |
Polling GET /api/payments/:id | Sederhana, tidak butuh alamat publik | Ada jeda dan request berulang |
| Pembeli kembali ke halaman sukses | Enak untuk tampilan | Tidak bisa dipercaya: siapa pun bisa membuka alamat itu sendiri |
Pakai webhook sebagai sumber utama dan polling sebagai cadangan. Jangan pernah menandai pesanan lunas hanya karena pembeli kembali ke redirect_url.
Isi webhook WaroengPay
WaroengPay mengirim POST berisi JSON saat status tagihan menjadi final. Event yang dikirim: payment.paid (lunas), payment.expired (kedaluwarsa), payment.cancelled (dibatalkan), dan ping dari tombol "Kirim tes" di dashboard.
// HeaderX-WaroengPay-Event: payment.paidX-WaroengPay-Delivery: 1842X-WaroengPay-Timestamp: 1790218992X-Signature: 5f2b0c...e91a // hex HMAC-SHA256(secret, "<timestamp>.<body>")// Body{"id": "evt_Qm2vK8sPz1Lx0aB7","event": "payment.paid","created_at": "2026-09-24T03:03:12.000Z","livemode": true, // false = event tagihan mode uji (sandbox)"data": {"id": "WRG-7K2M9QX4BD1PZ","reference": "INV-2041","status": "paid","base_amount": 50000,"amount": 50137,"pay_url": "https://waroengpay.com/pay/WRG-7K2M9QX4BD1PZ","paid_at": "2026-09-24T03:03:12.000Z","late": false, // true = pembayaran telat yang dicocokkan admin (bisa setelah payment.expired)"livemode": true,"link_id": null, // angka = dibuat pembeli lewat link pembayaran /l/<slug>"payment": {"issuer": "DANA","payer": "BU**I SAN**SO","bca_ref": "236433285","transaction_at": "2026-09-24T03:03:00.000Z"}}}
Header X-Signature adalah tanda tangan HMAC-SHA256 dari teks <timestamp>.<body> dengan secret webhook-mu (whsec_...). HMAC menggabungkan secret dengan isi pesan: hanya pihak yang memegang secret yang bisa membuat tanda tangan yang cocok, dan satu karakter body yang berubah menghasilkan tanda tangan yang sama sekali berbeda.
Lima aturan memproses webhook dengan aman
1. Verifikasi tanda tangan dari body mentah
Hitung ulang HMAC dari body persis seperti yang diterima, lalu bandingkan dengan fungsi yang waktunya konstan: hash_equals di PHP, crypto.timingSafeEqual di Node.js, atau hmac.compare_digest di Python. Jangan menghitung dari JSON yang sudah di-parse lalu diubah lagi menjadi teks, karena urutan kunci dan spasinya bisa berubah sehingga tanda tangannya tidak cocok.
2. Tolak kiriman lama
Timestamp ikut ditandatangani. Tolak webhook yang timestamp-nya berbeda lebih dari 5 menit dari jam servermu, supaya kiriman asli yang disadap tidak bisa dikirim ulang di kemudian hari (replay attack). Pastikan jam servermu sinkron.
3. Cocokkan pesanan lewat id tagihan dan nominal
Tanda tangan yang valid hanya membuktikan event berasal dari WaroengPay, bukan bahwa event itu untuk pesanan yang kamu kira. Orang yang memegang API key atau sesi dashboard-mu bisa membuat tagihan Rp1.000 dengan reference milik pesanan Rp5.000.000, membayarnya, dan webhook-nya tetap asli. Karena itu:
- Simpan
idtagihan (WRG-...) di pesanan saat tagihan dibuat, lalu cari pesanan lewatdata.id, bukandata.reference. - Pastikan
data.base_amountsama dengan total pesanan. Beda sedikit saja, jangan tandai lunas dan cek manual. - Abaikan event dengan
livemode: falsedi server produksi, karena itu event tagihan mode uji.
4. Proses setiap event sekali saja
Event yang sama bisa terkirim lebih dari sekali, misalnya saat balasan servermu terlambat. Simpan id event (evt_...) yang sudah diproses lalu lewati duplikatnya, atau cek dulu apakah pesanannya sudah lunas.
5. Balas cepat, proses berat belakangan
Balas dengan status 2xx dalam 10 detik. Pekerjaan berat seperti mengirim email atau membuat akun sebaiknya dimasukkan ke antrean dan dikerjakan setelah membalas.
Contoh kode verifikasi
Contoh berikut menerapkan kelima aturan di atas. Pilih bahasanya:
import crypto from "node:crypto";import express from "express";const app = express();const SECRET = process.env.WAROENGPAY_WEBHOOK_SECRET; // whsec_...// pakai body mentah: tanda tangan dihitung dari teks persis yang dikirimapp.post("/webhook/waroengpay", express.raw({ type: "application/json" }), async (req, res) => {const ts = req.get("x-waroengpay-timestamp") || "";const sig = req.get("x-signature") || "";const expected = crypto.createHmac("sha256", SECRET).update(`${ts}.${req.body}`).digest("hex");const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300; // tolak kiriman ulang > 5 menitif (!fresh || sig.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {return res.sendStatus(401);}const event = JSON.parse(req.body);if (event.livemode === false) return res.sendStatus(200); // event mode uji: jangan pernah menandai pesanan asliif (event.event === "payment.paid") {// cari pesanan lewat id tagihan yang disimpan saat membuat tagihan, BUKAN reference (tidak unik, bebas diisi)const order = await db.orders.findByWaroengpayId(event.data.id);if (!order || event.data.base_amount !== order.total) return res.sendStatus(200); // nominal beda: jangan tandai lunasawait db.orders.markPaid(order.id, event.id); // lewati kalau event.id sudah pernah diproses}res.sendStatus(200);});
Kalau servermu sedang gangguan
Webhook yang gagal, yaitu tidak dibalas 2xx dalam 10 detik, dikirim ulang dengan jeda bertahap: langsung, lalu setelah 30 detik, 2 menit, 10 menit, 30 menit, dan 2 jam. Totalnya 6 percobaan dalam sekitar 2,7 jam. Setelah itu:
- Buka tagihannya di dashboard lalu tekan Kirim ulang untuk mengirim ulang event terakhirnya.
- Atau ambil status terbarunya lewat
GET /api/payments/:id. Fieldwebhooksdi respons berisi 20 riwayat pengiriman terakhir.
Urutan event yang perlu diantisipasi
Biasanya satu tagihan hanya menghasilkan satu event final. Pengecualiannya adalah pembeli yang tetap membayar setelah tagihan kedaluwarsa atau dibatalkan. Admin WaroengPay bisa mencocokkan pembayaran itu secara manual, dan kamu menerima payment.paid dengan data.late = true setelah sebelumnya menerima payment.expired atau payment.cancelled. Jangan tolak event ini hanya karena pesanannya sudah ditandai kedaluwarsa: proses pesanannya, atau hubungi pembeli untuk pengembalian dana.
Alamat webhook yang diterima
- Harus bisa diakses dari internet lewat port 80, 443, 8080, atau 8443.
- Alamat jaringan privat seperti localhost, 10.x, 192.168.x, dan metadata cloud ditolak, dan redirect tidak diikuti.
- Untuk mencoba dari komputer lokal, pakai layanan tunnel seperti ngrok atau Cloudflare Tunnel.
- Alamat webhook dicatat saat tagihan dibuat: dari
callback_urltagihan itu, atau URL default akun di dashboard → Integrasi & API → Webhook.
Cara menguji
- Buat tagihan dengan API key mode uji (
wp_test_...) dan isicallback_url. Webhook tagihan uji hanya dikirim kecallback_urlyang diisi saat membuat tagihan, dan ditandatangani secret mode uji (whsec_test_...). - Lunasi tagihan uji dengan tombol Simulasikan bayar di halaman bayarnya, atau lewat API:
curl -X POST https://waroengpay.com/api/payments/WRG-7K2M9QX4BD1PZ/simulate \-H "Authorization: Bearer $WAROENGPAY_TEST_KEY" \-H "Content-Type: application/json" \-d '{"action":"pay"}' # atau "expire"
Sebelum go live, ganti ke secret live, isi URL webhook default di dashboard, lalu tekan Kirim tes. Dashboard mengirim event ping yang ditandatangani secret live; pastikan servermu membalas 2xx.
Dokumentasi lengkapnya ada di bagian webhook dan sandbox dokumentasi API. Untuk contoh integrasi dari awal sampai akhir, baca tutorial integrasi QRIS di PHP dan Laravel.