Blog · Terbit
Satu baca per detik per transaksi: membaca status pembayaran lewat API tanpa ditolak 429, dan kenapa transaksi yang sudah selesai dijawab dari cache
Mulai 10 Oktober 2026, GET /v1/transactions/:id menjawab paling banyak sekali per detik untuk satu transaksi per API key. Bacaan kedua atas transaksi yang sama di detik yang sama ditolak dengan 429 rate_limited dan header Retry-After. Transaksi lain tidak terpengaruh, jadi integrasi yang membaca banyak transaksi berbeda tetap berjalan. Yang kena adalah satu pola: loop rapat yang menanyakan transaksi yang sama berulang-ulang sambil menunggu statusnya berubah. Jalan keluarnya bukan menurunkan kecepatan loop itu, melainkan menjadikan webhook payment.paid sebagai sumber kabar, dan membaca status hanya di titik yang memang butuh jawaban saat itu juga.
Kenapa batas ini ada
Batas ini lahir dari satu kasus: endpoint webhook milik penjual terus gagal menerima, lalu servernya menanyakan transaksi yang sama tanpa jeda. Setiap pertanyaan itu memakai jatah 300 permintaan per menit milik key-nya, sehingga pembuatan pembayaran baru dari key yang sama ikut ditolak dengan 429. Penjualnya kehilangan pembayaran baru demi menanyakan pembayaran lama. Batas per transaksi memotong pola itu sebelum jatah gabungannya habis, dan transaksi yang sudah selesai kini dijawab dari cache, bukan dari database.
Cara kerja jendelanya
Jendelanya satu detik, dimulai dari bacaan pertama, untuk pasangan key dan transaksi. Bacaan pertama dijawab dan membuka jendela; bacaan berikutnya di dalam jendela yang sama ditolak, dan penolakan tidak memperpanjang jendela. Setiap jawaban dari endpoint ini membawa header batasnya:
HTTP/1.1 200 OK
RateLimit-Limit: 1
RateLimit-Remaining: 0
RateLimit-Reset: 1RateLimit-Remaining: 0 pada jawaban 200 itu wajar: satu-satunya jatah di jendela ini baru saja dipakai. Kalau dibaca lagi sebelum RateLimit-Reset habis, jawabannya:
HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 1
RateLimit-Remaining: 0
RateLimit-Reset: 1
Retry-After: 1
{ "error": { "code": "rate_limited",
"message": "at most one read per second per transaction; use webhooks for status updates" } }Dua proses yang memakai key yang sama untuk transaksi yang sama berbagi satu jendela. Kalau ada worker antrean dan halaman kembali yang sama-sama membaca satu transaksi, salah satunya akan sering ditolak.
Dua 429 dengan kode yang sama
Sejak batas ini ada, kode rate_limited pada /v1 bisa berarti dua hal, dan tindakannya berbeda:
| Batas | Berlaku untuk | Tanda pada jawaban | Tindakan |
|---|---|---|---|
| 1 per detik | Satu transaksi, satu key, hanya di GET per id | Ada Retry-After dan RateLimit-* | Tunggu sesuai Retry-After, lalu baca lagi transaksi itu saja |
| 300 per menit | Semua permintaan /v1 dari satu key | Tanpa Retry-After | Hentikan semua pembacaan dari key itu dan beri ruang untuk pembuatan pembayaran |
Bedakan keduanya dari ada tidaknya header Retry-After, bukan dari isi pesan. Pesan dicatat ke log untuk dibaca manusia, tetapi kode dan header itulah yang dipakai program. Jendela 300 per menit, lonjakan penerbitan tagihan, dan batas 120 per menit di halaman checkout dibahas di tulisan tentang rate limit dan lonjakan permintaan.
Satu hal yang paling sering terlewat: bacaan yang ditolak oleh batas per detik tetap dihitung terhadap jatah 300 per menit, karena pemeriksaan per key berjalan lebih dulu. Loop yang langsung mengulang setiap kali ditolak, misalnya sepuluh kali per detik, menghabiskan jatah 300 dalam 30 detik. Batas baru ini melindungi database, bukan melindungi integrasi dari loop-nya sendiri.
Transaksi yang sudah selesai dijawab dari cache
Jawaban untuk transaksi yang statusnya sudah final disimpan sementara, dan lamanya bergantung pada statusnya:
succeeded,failed, dancanceled: sampai satu jam. Status ini tidak berubah lagi, jadi tidak ada yang tertinggal.expired: hanya satu menit, karena konfirmasi pembayaran yang terlambat masih bisa mengubahnya menjadisucceeded. Dalam menit itu, bacaan bisa masih menjawab expired padahal uangnya sudah masuk.pending: tidak pernah disimpan, karena inilah status yang sedang ditunggu berubah.
Akibat praktisnya ada di baris expired. Jangan memperlakukan bacaan expired sebagai vonis final yang mengunci pesanan. Lepas stoknya dengan cara yang bisa diambil kembali, dan biarkan webhook payment.paid yang menyusul memulihkan pesanan. Urutan event itu dan event payment.expired dijelaskan di dokumentasi webhook.
Jadwal membaca di halaman kembali
Halaman kembali adalah satu-satunya tempat yang wajar untuk membaca status secara aktif: pembeli baru saja kembali dan butuh layar yang menjawab. Bacaan dilakukan dari server penjual dengan secret key, tidak pernah dari browser. Jadwal yang tidak pernah menabrak batas per detik dan berhenti dengan sopan:
// Halaman kembali: baca status dari server sendiri, bukan dari browser.
const JEDA = [0, 1500, 2000, 3000, 5000, 8000, 13000]; // ms, total ~32 detik
async function bacaStatus(id) {
for (const jeda of JEDA) {
await new Promise((r) => setTimeout(r, jeda));
const res = await fetch(`https://pay.kasera.id/v1/transactions/${id}`, {
headers: { Authorization: `Bearer ${process.env.KASERA_SECRET_KEY}` },
});
if (res.status === 429) {
const tunggu = Number(res.headers.get("Retry-After") ?? 60);
await new Promise((r) => setTimeout(r, tunggu * 1000));
continue;
}
const tx = await res.json();
if (tx.status !== "pending") return tx.status;
}
return "pending"; // tampilkan "sedang diperiksa", biarkan webhook yang memutuskan
}Tujuh bacaan dalam sekitar 32 detik untuk satu pembeli. Kalau statusnya masih pending setelah itu, tampilkan “pembayaran sedang diperiksa”, bukan “gagal”; Virtual Account bisa dikonfirmasi lebih lambat bergantung pada bank pembeli. Kenapa halaman kembali tidak boleh memutuskan apa pun, dan kenapa status di parameter URL-nya diabaikan, dibahas di perbandingan webhook dan halaman kembali sebagai pemicu.
Rekonsiliasi: pakai endpoint daftar, bukan bacaan per id
Pekerjaan malam yang memeriksa ulang semua pesanan yang belum lunas sering ditulis sebagai satu GET per pesanan. Untuk 200 pesanan, itu 200 permintaan. Endpoint daftar GET /v1/transactions mengembalikan sampai 100 baris per permintaan, dengan rentang created_after dan created_before, jadi pekerjaan yang sama selesai dalam dua atau tiga permintaan dan tidak menyentuh batas per transaksi sama sekali. Satu catatan dari dokumentasinya: filter status hanya berlaku pada halaman yang diambil, jadi telusuri halamannya tanpa filter lalu saring di sisi sendiri. Parameternya ada di referensi endpoint daftar.
Susunan yang bertahan di bawah batas ini sama dengan susunan yang memang dianjurkan sebelum batasnya ada: webhook sebagai jalur utama, bacaan per id hanya di halaman kembali dan saat pembeli bertanya, dan endpoint daftar untuk sapuan berkala. Hitungan lengkap kapan pembacaan berkala masih layak ada di perbandingan webhook dan menanyakan status berkala.
Pertanyaan yang sering muncul
Apakah membaca 50 transaksi berbeda dalam satu detik ikut ditolak?
Tidak. Batas sekali per detik berlaku per transaksi per API key, jadi 50 transaksi berbeda masing-masing punya jatah sendiri. Yang membatasi bacaan sebanyak itu adalah jatah gabungan 300 permintaan per menit per key, yang juga dipakai pembuatan pembayaran. Untuk memeriksa banyak transaksi sekaligus, endpoint daftar GET /v1/transactions mengembalikan sampai 100 baris dalam satu permintaan.
Bacaan saya ditolak 429. Apakah bacaan yang ditolak tetap dihitung?
Ya, terhadap jatah 300 permintaan per menit. Pemeriksaan per key berjalan lebih dulu, baru batas per transaksi. Jadi loop yang terus mengulang begitu ditolak tidak menjadi gratis karena ditolak: sepuluh percobaan per detik menghabiskan jatah 300 itu dalam setengah menit, dan sesudahnya POST /v1/transactions dari key yang sama ikut ditolak.
Transaksi saya expired, lalu pembeli ternyata membayar. Kenapa GET masih menjawab expired?
Transaksi berstatus expired dijawab dari cache selama paling lama satu menit, karena konfirmasi pembayaran yang terlambat masih bisa mengubahnya menjadi succeeded. Dalam rentang itu bacaan bisa tertinggal, tetapi webhook payment.paid tetap dikirim begitu pembayarannya tercatat. Itulah sebabnya stok yang dilepas saat expired harus bisa diambil kembali.