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.