Blog · 3 September 2026
Webhook tidak sampai: memisahkan belum pernah dikirim dari ditolak
Pembayaran berstatus succeeded, uangnya masuk, dan pesanan di sistem toko tidak pernah berubah. Reaksi yang paling sering adalah membuka kode handler dan membaca ulang verifikasi tanda tangan. Itu langkah kedua, bukan pertama. Langkah pertama adalah menjawab satu pertanyaan yang memisahkan seluruh kemungkinan menjadi dua kelompok: event itu pernah dikirim ke suatu alamat, atau tidak pernah dikirim ke mana pun.
Kalau tidak pernah dikirim, tidak ada yang salah dengan handler dan membacanya sepanjang apa pun tidak akan menemukan apa-apa. Log pengiriman di dashboard, pada menu Developer, menjawab pertanyaan itu dalam satu kata per event.
Lima jawaban, dan penyebabnya masing-masing
Kolom status pada log menampilkan lima kemungkinan, dan masing-masing menunjuk ke arah yang berbeda. Membaca yang benar menghemat sebagian besar waktu penelusuran.
| Status | Artinya | Yang harus diperiksa |
|---|---|---|
| Terkirim | Endpoint menjawab 2xx | Masalahnya di dalam handler, bukan pada pengiriman |
| Mencoba ulang | Ditolak, tetapi jatah percobaan masih ada | Kode jawaban pada percobaan terakhir |
| Gagal permanen | Semua percobaan habis ditolak | Perbaiki endpoint, lalu kirim ulang |
| Tidak pernah dikirim | Tidak ada URL endpoint saat event menunggu | Simpan URL endpoint, lalu kirim ulang |
| Dijeda oleh kami | Pengiriman ditahan selama akun ditinjau | Tidak ada, pengiriman lanjut sendiri |
Saringan status di log juga menyediakan pilihan Ditolak, yang mencakup dua status sekaligus: yang ditolak dan masih mencoba, serta yang ditolak sampai jatahnya habis. Itu saringan yang tepat untuk pertanyaan “apa saja yang pernah ditolak endpoint ini”, terlepas dari apakah masih ada harapan atau tidak.
Tidak pernah dikirim: status yang paling sering salah dibaca
Ini penyebab yang paling banyak membuang waktu, karena tampilannya di sisi toko persis sama dengan endpoint yang bermasalah: pesanan tidak berubah, dan tidak ada apa pun di log server sendiri. Bedanya, di sisi kami event itu tidak pernah diposkan ke mana pun, jadi tidak ada yang menolaknya dan jumlah percobaannya masih nol.
Ada tiga cara sampai ke sana:
- Belum ada endpoint yang disimpan sama sekali saat pembayaran terjadi.
- Endpoint ada tetapi dibuat lebih dulu tanpa URL, yaitu cara mendapatkan signing secret sebelum handler dibangun.
- Endpoint ada dan berisi URL, tetapi sedang dinonaktifkan.
Yang penting diketahui: ketiganya menahan event, bukan menggagalkannya, dan tidak membakar satu pun percobaan ulang. Menyimpan URL atau mengaktifkan kembali endpoint membangunkan event yang tertahan, sehingga riwayat terbaru tetap terkirim tanpa perlu meminta apa pun. Itulah alasan menonaktifkan endpoint adalah cara yang benar untuk menghadapi pemeliharaan terjadwal, dan membiarkan endpoint menjawab 500 adalah cara yang salah.
Penahanan itu ada batasnya. Event yang tidak punya tujuan ditahan sampai tujuh hari sejak dibuat, lalu ditutup tanpa pernah terkirim. Satu minggu cukup untuk memasang handler; setelah itu event tersebut memang sudah basi.
Tangga percobaan ulang, dan cara menyetel peringatan sendiri
Kalau endpoint menjawab apa pun selain 2xx, atau tidak bisa dihubungi sama sekali, event masuk ke tangga percobaan ulang. Percobaan pertama berjalan seketika, lalu tiap kegagalan menjadwalkan yang berikutnya dengan jarak yang melebar: satu menit, lima menit, tiga puluh menit, dua jam, enam jam, dan dua puluh empat jam. Totalnya tujuh percobaan yang terentang sekitar 33 jam. Kebijakan ini juga tercetak di kartu webhook pada dashboard, dengan angkanya diambil langsung dari sistem, dan diringkas di dokumentasi webhook.
Rentang itu dipilih supaya deploy yang gagal pada Jumat sore masih tertolong pada Sabtu malam. Konsekuensi praktisnya ada dua. Pertama, endpoint yang mati kurang dari satu jam hampir selalu sembuh sendiri tanpa ada yang perlu dikerjakan. Kedua, peringatan internal sebaiknya disetel jauh lebih rapat daripada 33 jam, karena menunggu jatahnya habis berarti baru menyadari masalah pada hari berikutnya. Selisih antara percobaan ketiga dan keempat, yaitu tiga puluh menit menjadi dua jam, adalah tempat yang masuk akal untuk memasang ambang peringatan sendiri.
Setelah percobaan terakhir habis, event berhenti dan hanya tombol kirim ulang yang menghidupkannya lagi. Kirim ulang memberi tepat satu percobaan tambahan, langsung saat itu juga, jadi urutannya selalu sama: perbaiki endpoint dulu, baru kirim ulang. Kirim ulang pada endpoint yang belum diperbaiki hanya menghabiskan satu percobaan lagi.
Yang dihitung sebagai penolakan
Beberapa penyebab kegagalan tidak pernah sampai ke kode handler, dan mencarinya di sana sia-sia. Semuanya tercatat apa adanya pada baris percobaan, yang bisa dibuka satu per satu di log lengkap dengan nomor percobaan, kode HTTP atau pesan galatnya, dan durasinya.
- Jawaban selain 2xx. 301, 302, dan 307 termasuk. Pengalihan tidak diikuti, jadi endpoint yang dipindahkan lewat redirect terbaca sebagai gagal. Perbarui URL endpoint ke alamat yang baru.
- Skema selain https. URL webhook wajib https. Ini ditolak sejak disimpan, bukan saat pengiriman.
- Alamat yang tidak publik. URL yang mengarah ke localhost, jaringan internal, atau alamat layanan metadata ditolak pada saat menyambung. Pemeriksaan itu dilakukan terhadap alamat yang benar-benar dihubungi, bukan hanya saat penyimpanan, jadi nama domain yang mengarah ke alamat privat tetap ditolak meski lolos ketika disimpan. Untuk pengembangan di komputer sendiri, gunakan terowongan https publik, bukan alamat lokal.
- Sertifikat atau waktu jawaban. Sambungan yang gagal atau tidak dijawab tepat waktu dicatat sebagai galat, bukan sebagai kode HTTP.
Menjawab 2xx pada payload yang gagal diproses adalah kesalahan yang berlawanan arah, dan konsekuensinya dibahas di artikel tentang webhook pembayaran yang aman: jawaban 2xx mengakhiri seluruh tangga percobaan, sehingga notifikasi terakhir yang akan pernah datang dibuang begitu saja.
Endpoint yang benar, mode yang salah
Satu penyebab “webhook tidak sampai” sama sekali tidak berhubungan dengan pengiriman: pengujian dilakukan di satu mode sementara endpoint dipasang di mode lain. Mode live dan mode sandbox adalah endpoint terpisah dengan signing secret masing-masing. Event sandbox hanya dikirim ke endpoint sandbox dan membawa livemode: false; event live hanya dikirim ke endpoint live. Secret sandbox tidak akan pernah bisa memverifikasi payload live, dan sebaliknya.
Gejalanya khas: pembayaran sandbox terlihat berhasil, log pengiriman kosong pada mode itu, dan handler sama sekali tidak tersentuh. Periksa mode yang sedang aktif di dashboard, lalu pastikan endpoint pada mode tersebut memang punya URL. Cara mengelola kedua secret itu tanpa tertukar dibahas di artikel tentang menyimpan dan merotasi kunci API.
Kalau ada lebih dari satu endpoint, misalnya satu untuk toko dan satu untuk sistem pencatatan, tiap endpoint menerima salinannya sendiri, ditandatangani dengan secret-nya sendiri, dan mencoba ulang sendiri-sendiri. Satu endpoint yang mati tidak pernah menunda endpoint yang lain, dan status di log dibaca per endpoint. Maksimalnya lima endpoint per mode.
Urutan memeriksa
- Buka log pengiriman pada mode yang sesuai, lalu cari eventnya. Kotak pencarian menerima id event, id pembayaran, atau
merchant_ref. - Baca statusnya. Kalau Terkirim, endpoint sudah menjawab 2xx dan penelusuran berpindah ke log aplikasi sendiri. Kalau Tidak pernah dikirim, perbaiki endpointnya dan berhenti membaca kode.
- Buka daftar percobaannya. Kode HTTP atau pesan galat pada tiap baris menunjuk penyebabnya langsung, dan pola waktunya menunjukkan kapan endpoint mulai bermasalah.
- Lihat panel kesehatan endpoint. Persentase keberhasilan dan grafik terkirim dibanding gagal per hari memisahkan satu event yang aneh dari endpoint yang memang sedang jatuh.
- Setelah perbaikan, gunakan tombol kirim event percobaan untuk memastikan jalurnya hidup, baru kirim ulang event yang tertinggal.
Satu hal yang tidak perlu dipantau sendiri: kalau endpoint berhenti menjawab, kami mengirim satu pemberitahuan lewat surel untuk seluruh gangguan itu, bukan satu surel per event, ditutup dengan pemberitahuan pemulihan saat pengiriman berhasil lagi.
Yang memang tidak akan pernah datang
Terakhir, sebagian laporan “webhook tidak sampai” sebenarnya menunggu event yang tidak pernah ada. Satu-satunya jenis event yang dikirim untuk pembayaran adalah payment.paid. Kedaluwarsa tidak pernah didorong lewat webhook, begitu pula pembatalan dan pengembalian dana. Pesanan yang perlu dilepas saat pembayaran kedaluwarsa harus dilepas berdasarkan expires_at dari sisi toko sendiri, bukan menunggu pemberitahuan. Langkah memasang jalur itu dari awal ada di panduan integrasi QRIS di website, dan arti tiap kode penolakan ada di daftar kode galat.