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"
}
}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))
);
});
}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.