Perbandingan · Terbit
Menunggu webhook atau menanyakan status berkala: yang benar-benar dijamin masing-masing, dan satu keadaan yang tidak pernah datang sendiri
Pertanyaannya muncul di hari pertama integrasi dan jawabannya menentukan bentuk sistemnya untuk waktu yang lama: menunggu webhook, atau menanyakan status pembayaran secara berkala. Jawaban yang paling sering benar bukan salah satunya, melainkan pembagian peran. Webhook dipakai sebagai pemicu, pembacaan status dipakai sebagai penentu sebelum barang dilepas, dan satu sapuan berkala yang jarang dipakai sebagai jaring untuk pesanan yang tertinggal.
Halaman ini membandingkan keduanya dari sisi jaminannya. Cara memasang webhook dan memverifikasi tanda tangannya ada di dokumentasi webhook, dan penelusuran saat webhook tidak sampai ada di bahasan webhook tidak sampai.
Yang benar-benar dijamin webhook
Pengirimannya bersifat at-least-once: event yang sama bisa datang lebih dari sekali dan selalu membawa id yang sama, jadi dedupe berdasarkan id event adalah bagian dari integrasinya, bukan penyempurnaan yang bisa ditunda. Endpoint wajib menjawab 2xx; kalau tidak, pengirimannya dicoba ulang dengan backoff sampai tujuh percobaan dalam kisaran 33 jam, lalu berhenti. Satu mode boleh memiliki sampai lima endpoint dengan secret masing-masing, dan satu endpoint yang menolak tidak menunda yang lain. Endpoint yang dinonaktifkan menahan event-nya tanpa membakar percobaan ulang.
Yang tidak dijamin webhook, dan paling sering diandaikan: kedatangannya sebelum pembeli kembali ke halaman toko. Karena itu memenuhi pesanan berdasarkan kepulangan pembeli adalah kesalahan yang terlihat benar sampai ada pembeli yang menutup halaman terlalu cepat. Verifikasi tanda tangan dan urutan pemrosesan yang aman dibahas di bahasan webhook pembayaran yang aman.
Yang benar-benar dijamin permintaan status
Pembacaan lewat GET /v1/transactions/{id} menjawab keadaan pada saat ditanyakan, dan tidak menjanjikan apa pun di luar saat itu. Kelebihannya bukan kecepatan, melainkan bahwa jawabannya tidak bergantung pada apa pun yang bisa hilang di jalan: tidak ada URL publik yang harus dijaga, tidak ada percobaan ulang yang bisa habis, dan tidak ada perbedaan mode endpoint yang bisa tertukar.
Satu perilaku pembacaan yang layak diketahui sebelum menulis penyapu: permintaan yang masih pending tetapi sudah melewati masa berlakunya dibaca sebagai expired pada setiap jalur pembacaan, tanpa perlu menunggu proses penyapu di sisi server menuliskan statusnya. Artinya sebuah pembacaan tidak pernah menjawab pending untuk sesuatu yang sebenarnya sudah mati. Nilai statusnya sendiri ada lima, yaitu pending, succeeded, expired, canceled, dan failed, jadi percabangan yang hanya menangani tiga nilai pertama akan jatuh pada dua nilai yang sah.
Satu keadaan yang tidak pernah datang sendiri
Ini fakta yang paling sering menjadi cacat pertama pada integrasi yang hanya mendengar: kedaluwarsa tidak pernah didorong sebagai webhook. Satu-satunya event pembayaran yang dikirim adalah payment.paid, ditambah satu event uji dari dashboard untuk membuktikan endpoint sudah terpasang. Tidak ada event untuk kedaluwarsa, tidak ada untuk pembatalan, tidak ada untuk kegagalan.
Akibatnya bersifat operasional, bukan teoretis. Stok yang ditahan saat tagihan dibuat akan tertahan selamanya kalau pelepasannya menunggu kabar; pesanan yang tidak dibayar akan menggantung di daftar pesanan berstatus menunggu. Yang melepaskannya hanya dua hal: expires_at yang dipegang di basis data sendiri, atau satu pembacaan status pada pesanan yang sudah lewat waktunya. Default masa berlakunya 60 menit dengan plafon 24 jam, dan permintaan yang melewati plafon itu ditolak dengan 422 expiry_too_long saat dibuat.
Anggaran polling, dihitung
Batasnya 300 permintaan per menit per kunci API untuk seluruh /v1/*, dan anggaran itu dipakai bersama oleh pembuatan pembayaran dan pembacaan lainnya. Jumlah pembayaran yang berjalan bersamaan yang membuat satu kunci mentok, kalau seluruh anggaran dipakai polling saja:
| Interval | Permintaan per menit per pembayaran | Pembayaran berjalan sampai mentok |
|---|---|---|
| 5 detik | 12 | 25 |
| 15 detik | 4 | 75 |
| 30 detik | 2 | 150 |
| 60 detik | 1 | 300 |
Dua catatan yang mengubah arti tabel itu. Pertama, angka paling kanan adalah plafon teori, bukan target: pembuatan pembayaran pada jam sibuk memakai anggaran yang sama. Kedua, halaman pembayaran yang dibuka pembeli menanyakan statusnya sendiri di luar anggaran ini, dengan batas terpisah per alamat IP, jadi status di layar pembeli tetap berubah sendiri tanpa memakai satu pun permintaan dari kunci API penjual.
Tiga jebakan yang hanya muncul pada pembacaan berkala
Ketiganya berupa penolakan yang jelas, bukan kegagalan senyap, dan ketiganya ditemukan paling murah sebelum penyapu dijalankan di produksi.
- Kursor yang bukan dari API ini. Parameter
starting_afterpada daftar permintaan pembayaran harus berupa id berawalanpayreq_, tepat seperti yang dikembalikan API. Uuid polos yang diambil dari basis data sendiri ditolak dengan422 invalid_cursor, bukan diabaikan. - Batas waktu yang bukan RFC3339. Nilai
created_afterdancreated_beforeharus lengkap beserta zona waktunya, misalnya2026-09-13T00:00:00+07:00. Tanggal polos ditolak dengan422 invalid_filter. Penolakan ini disengaja: filter yang diam-diam tidak berlaku akan mengembalikan daftar tanpa saringan, dan itu terbaca sebagai tidak ada yang cocok. - Halaman penuh terakhir. Penanda
has_morebernilai benar setiap kali satu halaman kembali penuh, jadi halaman penuh yang kebetulan yang terakhir tetap menjanjikan halaman berikutnya, yang kemudian kembali kosong. Hentikan perulangan pada halaman kosong, bukan pada penandanya saja. Nilailimitdi luar rentang 1 sampai 100 juga diganti diam-diam menjadi 20, jadi permintaan yang terasa lambat kadang sebenarnya hanya mengambil 20 baris per panggilan.
Susunan tiga lapis kalau keduanya dipakai bersama
Ini bentuk yang dipakai kebanyakan integrasi yang sudah berjalan lama, dan setiap lapisnya ada karena satu kegagalan nyata.
- Webhook sebagai pemicu. Verifikasi tanda tangan, dedupe berdasarkan id event, jawab 2xx secepat mungkin, lalu kerjakan sisanya di belakang. Lapis ini yang membuat pesanan terpenuhi dalam hitungan detik.
- Pembacaan status sebagai penentu. Sebelum barang dilepas atau akses dikirim, baca permintaan pembayarannya lewat API dan cocokkan status beserta nominalnya dengan pesanan sendiri. Yang dipercaya adalah pembacaan itu, bukan isi payload yang datang.
- Sapuan berkala sebagai jaring. Setiap sepuluh sampai lima belas menit, ambil dari basis data sendiri pesanan yang masih menunggu dan sudah melewati
expires_at, lalu tanyakan statusnya satu per satu berdasarkan id. Sapuan seperti ini tidak membutuhkan penyaringan di sisi API sama sekali, karena daftar yang perlu ditanyakan sudah ada di basis data sendiri, dan ukurannya kecil justru karena dua lapis di atasnya sudah menutup jalur normalnya.
Kalau hanya satu yang bisa dibangun
Ada keadaan yang membuat pilihannya tidak bebas, dan jawabannya berbeda per keadaan.
- Tidak ada server yang bisa menerima HTTPS publik. Penjualan yang dijalankan dari mesin sendiri, alat tanpa kode, atau lembar kerja tidak bisa menerima webhook, karena URL-nya wajib https dan mengarah ke alamat publik. Pembacaan berkala adalah satu-satunya jalan, dengan interval 30 sampai 60 detik dan
expires_atdipegang sendiri. - Masih di komputer sendiri saat membangun. Ini bukan alasan memilih polling, melainkan soal mode dan terowongan; caranya ada di bahasan webhook di localhost dan mode tes.
- Lebih dari satu sistem perlu tahu. Toko dan sistem pencatatan yang keduanya perlu diberi tahu lebih murah dilayani webhook, karena lima endpoint per mode masing-masing menerima salinannya sendiri dan dicoba ulang sendiri-sendiri. Polling berarti setiap sistem memakai anggaran kunci API yang sama.
- Lebih dari sekitar dua puluh lima pembayaran berjalan bersamaan. Polling lima detik tidak lagi tersedia pada volume itu, dan menaikkan intervalnya berarti menambah jeda sebelum pesanan terpenuhi. Di titik ini webhook berhenti menjadi pilihan gaya dan menjadi syarat.
Satu hal yang tidak berubah di semua keadaan: yang melepaskan stok tetap expires_at, karena tidak ada kabar yang datang saat sebuah tagihan mati.
Pertanyaan yang sering muncul
Ada webhook untuk pembayaran yang kedaluwarsa, dibatalkan, atau gagal?
Tidak. Satu-satunya event pembayaran yang didorong adalah payment.paid, ditambah satu event uji yang bisa dikirim dari dashboard untuk membuktikan endpoint sudah terpasang. Tidak ada event untuk kedaluwarsa, pembatalan, maupun kegagalan, sehingga pelepasan stok dan penutupan pesanan harus dijalankan dari expires_at yang dipegang sendiri atau dari satu pembacaan status.
Berapa lama menunggu sebelum sebuah permintaan pembayaran dianggap tidak akan dibayar?
Tepat sampai expires_at yang tercatat pada permintaan itu, bukan sampai batas waktu yang ditebak sendiri. Default masa berlakunya 60 menit dengan plafon 24 jam per akun, dan permintaan yang melewati plafon itu ditolak dengan 422 expiry_too_long saat dibuat. Setelah expires_at lewat, pembacaan status mana pun sudah menjawab expired tanpa perlu menunggu apa pun.
Pembacaan status berkala bisa kena batas laju?
Bisa, dan batasnya satu untuk semua: 300 permintaan per menit per kunci API untuk seluruh /v1/*, dijawab dengan 429 rate_limited kalau terlampaui. Anggaran itu dipakai bersama oleh pembuatan pembayaran, pembacaan status, dan pengembalian dana, jadi interval polling perlu dihitung terhadap jumlah pembayaran yang berjalan bersamaan, bukan ditetapkan sekali sebagai lima detik.
Kalau webhook sudah diverifikasi tanda tangannya, masih perlu membaca status lewat API?
Perlu, untuk keputusan yang melepaskan barang atau mengirim akses. Tanda tangan yang sah membuktikan pengirimnya, bukan bahwa pesanan yang dimaksud memang pesanan yang ada di sistem sendiri dengan nominal yang sama. Membaca status dan nominal lewat GET /v1/transactions/{id} sebelum memenuhi pesanan membuat pembacaan itu, bukan isi payload, yang menjadi penentu terakhir.