Event
Webhook
Terima payment.paid ber-signature begitu pembayaran terkonfirmasi.
Tambahkan webhook endpoint di dashboard (menu Developer) — maksimal lima per mode, masing-masing dengan URL, nama, dan signing secret sendiri, jadi toko dan ERP bisa menerima kiriman masing-masing. Saat pembayaran terkonfirmasi, kami mengirim POST ke setiap endpoint yang aktif, dengan payload:
{
"id": "evt_...",
"type": "payment.paid",
"livemode": false,
"created_at": "2026-08-11T12:05:00+07:00",
"data": {
"payment_request_id": "payreq_9b2f...",
"external_id": "ORD-1234",
"merchant_ref": "INV-2026-001",
"amount": 150000,
"currency": "IDR",
"customer": { "name": "Budi", "email": "budi@toko.dev" },
"paid_at": "2026-08-11T12:04:58+07:00",
"status": "succeeded"
}
}Event yang diterima
Ada tiga event pembayaran. Setiap endpoint memilih sendiri mana yang ingin diterima, lewat dashboard; endpoint yang dibuat sebelum pilihan ini ada hanya menerima payment.paid, dan tetap begitu sampai diubah.
payment.paid— pembeli sudah membayar. Ini yang berarti uang masuk.payment.expired— permintaan lewat batas waktu tanpa dibayar.payment.failed— rail menolak permintaannya. Ini bukan percobaan yang gagal: pembeli yang scan-nya gagal sekali masih bisa scan lagi, dan permintaannya tetap terbuka. Hanya permintaan yang benar-benar mati yang mengirim ini.
Dua event terminal ini bentuknya sama dengan yang paid, dengan paid_at: null:
{
"id": "evt_...",
"type": "payment.expired",
"livemode": true,
"created_at": "2026-08-11T13:00:00+07:00",
"data": {
"payment_request_id": "payreq_9b2f...",
"external_id": "ORD-1234",
"amount": 150000,
"currency": "IDR",
"paid_at": null,
"status": "expired"
}
}Expired bisa disusul pembayaran. Kalau uang pembeli sudah diterima sebelum timer expiry kami jalan, uang yang menang: permintaan jadi succeeded dan payment.paid menyusul payment.expired yang sudah dikirim. Baca data.status — itu status pembayaran saat event dibuat, bukan saat diantrekan — atau ambil ulang permintaannya untuk memastikan. Kalau stok sudah dilepas saat expired, event paid adalah sinyal untuk menariknya kembali atau melakukan refund.
Pengiriman bersifat at-least-once: event yang sama bisa terkirim lebih dari sekali dan selalu membawa id yang sama — dedupe berdasarkan id. Kalau ada beberapa endpoint, masing-masing menerima event satu kali dan di-retry sendiri-sendiri: satu endpoint yang menolak tidak akan menunda yang lain. Endpoint Anda harus menjawab 2xx; kalau tidak, kami retry dengan exponential backoff maksimal 7 kali dalam ±33 jam. URL webhook wajib https dan mengarah ke alamat publik. Live dan test adalah endpoint terpisah dengan signing secret masing-masing: atur masing-masing di mode dashboard-nya sendiri. Event test mode hanya dikirim ke endpoint test dan membawa livemode: false; event live hanya ke endpoint live. Secret test tidak akan pernah bisa memverifikasi payload live.
Verifikasi signature
Setiap pengiriman membawa signature dengan timestamp di header Kasera-Signature-V1: t=<unix>,v1=<hex>, dengan v1 adalah HMAC-SHA256 atas t + "." + rawBody. Verifikasi terhadap t dan tolak pengiriman yang timestamp-nya melenceng lebih dari 5 menit dari jam server Anda — ini yang mencegah pengiriman yang disadap di-replay belakangan. Setelah rotasi secret, header membawa dua entri v1 selama 24 jam — satu per secret — jadi terima pengiriman kalau salah satu entri cocok. Id event juga ada di Kasera-Event-Id.
// Node.js
const crypto = require("crypto");
// Kasera-Signature-V1: t=1723350300,v1=5f4d...[,v1=9a1b...]
function verify(rawBody, v1Header, secret, toleranceSeconds = 300) {
const parts = v1Header.split(",");
const t = Number(parts[0].slice(2)); // "t=<unix>"
if (!Number.isFinite(t)) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(t + "." + rawBody)
.digest("hex");
return parts
.slice(1)
.filter((p) => p.startsWith("v1="))
.some((p) => {
const sig = Buffer.from(p.slice(3));
return (
sig.length === expected.length &&
crypto.timingSafeEqual(sig, Buffer.from(expected))
);
});
}Di PHP atau JavaScript/TypeScript Anda tidak perlu kode ini: SDK resmi sudah menyediakannya sebagai Webhook::constructEvent dan constructWebhookEvent, lengkap dengan toleransi timestamp dan masa transisi rotasi secret.
Header lama (deprecated)
Pengiriman masih membawa header Kasera-Signature yang asli — hex HMAC-SHA256 atas raw body, tanpa timestamp — jadi kode verifikasi yang sudah ada tetap jalan tanpa perubahan. Header ini deprecated: tidak melindungi dari replay, dan hanya di-sign dengan secret saat ini (tanpa grace period rotasi). Pindahlah ke Kasera-Signature-V1; header lama akan dihapus setelah masa deprecation yang diumumkan lebih dulu.
// Node.js — legacy Kasera-Signature (deprecated)
const crypto = require("crypto");
function verifyLegacy(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signatureHeader),
Buffer.from(expected)
);
}Secret bisa dilihat kapan saja di dashboard, dan hanya berubah saat Anda merotasinya — memindahkan URL webhook ke domain baru tidak mengganti secret, jadi migrasi tidak perlu deploy ulang kode verifikasi Anda. Rotasi punya grace period 24 jam: secret lama tetap men-sign entri v1 kedua di samping yang baru, jadi deploy secret baru tanpa terburu-buru — retry yang sedang berjalan tetap lolos verifikasi. Anda juga bisa menambah endpoint tanpa URL untuk dapat secret-nya dulu: buat dan deploy handler-nya, arahkan domain belakangan. Endpoint yang dinonaktifkan menahan event-nya tanpa menghabiskan jatah retry sampai Anda aktifkan lagi.
Verifikasi signature sebelum memproses payload. Pakai raw body, bukan JSON yang di-parse ulang.