Event yang tersedia
| Event | Dikirim ketika | Tindakan |
|---|---|---|
| transaction.paid | Pembayaran cocok dengan transaksi | Verifikasi lalu penuhi order |
| transaction.expired | Waktu pembayaran berakhir | Tutup checkout |
| webhook.test | Tes dikirim dari dashboard | Validasi koneksi |
Dapatkan webhook secret
Setiap project punya satu webhook secret sendiri, format whsec_ diikuti 48 karakter acak. Secret dibuat otomatis saat pertama kali Anda mengisi Webhook URL di /dashboard/projects, dan ditampilkan hanya SATU KALI saat itu — salin dan simpan di secret manager Anda sebelum meninggalkan halaman. Kalau lupa atau dicurigai bocor, klik "Buat Ulang Secret" untuk membuat yang baru (yang lama langsung berhenti berlaku).
- Simpan sebagai environment variable, bukan di source code
- Beda project berarti beda secret — jangan pakai ulang antar project
- Rotasi secret tidak mengubah Webhook URL, jadi tidak ada downtime saat mengganti
Header yang dikirim di setiap webhook
| Header | Isi |
|---|---|
| X-Webhook-Event | Nama event, sama dengan field event pada body |
| X-Webhook-Delivery | ID delivery unik dan stabil — dipakai ulang persis sama kalau KiPay mencoba mengirim ulang event yang sama |
| X-Webhook-Timestamp | Waktu pengiriman, ikut ditandatangani untuk mencegah replay |
| X-Webhook-Signature | HMAC-SHA256, format sha256=<hex> |
Verifikasi signature sebelum memproses payload
Signature dihitung dari HMAC-SHA256 atas string "{timestamp}.{deliveryId}.{rawBody}" memakai webhook secret Anda. Pakai raw body mentah persis seperti yang diterima — body yang sudah di-parse lalu di-serialize ulang bisa mengubah urutan key atau whitespace dan membuat signature tidak cocok.
import { verifyWebhookSignature } from 'kipay-node';
app.post(
'/webhooks/kipay',
express.raw({ type: 'application/json' }), // JANGAN express.json() di route ini
async (req, res) => {
const rawBody = req.body.toString('utf8');
const ok = verifyWebhookSignature({
rawBody,
secret: process.env.KIPAY_WEBHOOK_SECRET!,
timestamp: req.header('X-Webhook-Timestamp') ?? '',
deliveryId: req.header('X-Webhook-Delivery') ?? '',
signature: req.header('X-Webhook-Signature') ?? '',
});
if (!ok) return res.status(401).send('invalid signature');
res.status(200).send('ok'); // ack dulu, proses berat lewat queue
const { event, transaction } = JSON.parse(rawBody);
if (event === 'transaction.paid') {
await queuePaymentVerification(transaction.trx_id);
}
}
);Algoritmanya sederhana: HMAC-SHA256(secret, `${timestamp}.${deliveryId}.${rawBody}`), lalu bandingkan hasilnya (hex, diawali "sha256=") dengan header X-Webhook-Signature memakai constant-time comparison. Tolak juga timestamp yang terlalu lama (mis. lebih dari 5 menit) untuk membatasi replay.
Verifikasi status sebelum memenuhi order
Signature yang valid membuktikan payload memang dari KiPay dan tidak diubah di tengah jalan — itu belum cukup untuk memenuhi order.
- 01Cari trx_id
Pastikan transaksi terhubung ke satu order yang Anda kenali.
- 02Baca status dari KiPay
Kirim request server-to-server dan pastikan status transaksi adalah paid.
- 03Bandingkan amount
Pastikan nominal final sesuai dengan order.
- 04Penuhi order satu kali
Gunakan trx_id sebagai idempotency boundary internal.
Kalau endpoint Anda gagal merespons
KiPay mencoba ulang pengiriman yang gagal (bukan 2xx, atau timeout) hingga 6 kali dengan jeda meningkat — sekitar 10 detik, lalu 20, 40, 80, dan 160 detik. Setiap percobaan ulang memakai X-Webhook-Delivery yang sama persis, jadi jadikan itu (bukan hanya trx_id) sebagai kunci idempotency kalau Anda ingin mendeteksi pengiriman duplikat secara eksplisit. Setelah 6 kali gagal, KiPay berhenti mencoba untuk event tersebut.