Webhook yang Tidak Membuat Uang Hilang

Webhook yang Tidak Membuat Uang Hilang
Webhook itu notifikasi otomatis antar-sistem: begitu ada kejadian, sistem A langsung "menelepon" sistem B — bukan menunggu ditanya. Payment gateway dan platform booking memakainya untuk mengabari sistem Anda soal event yang menyangkut uang: pembayaran berhasil, booking terkonfirmasi, langganan diperpanjang, pesanan terpenuhi. Diam-diam, kiriman inilah yang jadi sumber kebenarannya.
Kebanyakan sistem yang saya tinjau masih memperlakukan pengiriman ini sebagai best-effort — dicoba sekali, sampai syukur, tidak sampai ya sudah — tanpa rencana untuk skenario "tidak sampai."
Di celah itulah refund terlewat, booking tidak pernah terkonfirmasi, dan tiket support menumpuk menanyakan kenapa pelanggan sudah bayar tapi sistem tidak tahu. Menutup celah ini tidak butuh infrastruktur yang aneh-aneh. Yang dibutuhkan cuma memperlakukan webhook sebagai masalah reliability, bukan kerjaan tambahan yang dipasang dalam satu sore.
Verifikasi dulu sebelum percaya isi kirimannya
Kesalahan pertama adalah memproses body webhook sebelum memastikan siapa pengirimnya. Siapa pun yang tahu URL endpoint sistem bisa mengirim POST palsu berisi event "payment.succeeded". Setiap provider serius menyertakan signature — tanda tangan digital — di tiap kiriman, persis seperti stempel lilin di surat lama — bukan mencegah orang membaca isinya, tapi membuktikan segelnya asli dan belum diutak-atik di jalan.
Stripe mengirim header Stripe-Signature berisi timestamp dan signature HMAC-SHA256 yang dihitung dari raw request body. Library resmi Stripe menerapkan toleransi default 5 menit antara timestamp dan waktu saat ini untuk memblokir replay attack. Jika verifikasi dilakukan manual, perbandingannya harus constant-time: perbandingan == biasa pada string signature membocorkan informasi timing yang bisa dieksploitasi penyerang. Header X-Hub-Signature-256 milik GitHub bekerja dengan cara serupa, dan dokumennya eksplisit: gunakan fungsi constant-time seperti crypto.timingSafeEqual, jangan pernah pakai perbandingan equality biasa.
Header X-Shopify-Hmac-Sha256 milik Shopify mengikuti pola identik: HMAC-SHA256 yang di-encode base64 dari raw body yang belum di-parse, dihitung dengan client secret app terkait. Detail "raw body yang belum di-parse" ini kelihatan sepele, padahal menentukan. Kalau framework yang digunakan sudah men-parse JSON sebelum pengecekan signature berjalan, hash yang dihitung sering kali berasal dari payload yang sudah di-serialize ulang.
Hash itu tidak lagi cocok dengan yang ditandatangani provider. Jadi selalu verifikasi raw bytes-nya, sebelum middleware apa pun menyentuhnya.
Jawab cepat, kerjakan yang sebenarnya belakangan
Provider tidak menunggu tanpa batas waktu. Stripe akan timeout kalau responsnya terlalu lama. Shopify mengharapkan respons dalam kira-kira 5 detik. GitHub memberi 10 detik.
Kalau pipeline fulfillment lengkap dijalankan inline (validasi charge, update inventori, email konfirmasi), cepat atau lambat batas waktu itu kelewat saat traffic tinggi. Dan dari sisi provider, endpoint yang lambat kelihatan sama persis dengan endpoint yang rusak.
Panduan resmi Stripe: kembalikan respons 2xx dulu, baru jalankan logika kompleksnya secara asinkron lewat queue — bukan inline di dalam request handler. Tugas handler cuma satu: verifikasi signature, tulis raw event ke queue atau tabel, kembalikan 200. Worker mengambilnya beberapa detik kemudian dan mengerjakan proses sebenarnya — jadi lonjakan traffic tidak berubah jadi retry storm, banjir kiriman ulang dari provider yang mengira endpoint mati, karena endpoint tetap cepat sepanjang apa pun antrean di belakangnya.
Dedupe berdasarkan event id, setiap saat
Retry bukan kejadian langka — retry justru tanda protokolnya bekerja sesuai desain. Stripe secara eksplisit memperingatkan bahwa endpoint bisa menerima event yang sama lebih dari sekali. Stripe merekomendasikan mencatat event ID yang sudah diproses, lalu melewati apa pun yang sudah tercatat. Shopify memberi alat yang sama lewat header X-Shopify-Webhook-Id, dibuat khusus untuk mendeteksi dan melewati pengiriman duplikat.
Lewatkan langkah ini, dan event payment.succeeded yang di-retry memenuhi pesanan yang sama dua kali, atau konfirmasi booking mengirim dua email duplikat ke tamu yang sama. Perbaikannya kecil: tabel idempotency yang di-key dengan event id, dicek sebelum proses dimulai, seperti petugas pintu yang mengecek gelang tamu sebelum mengizinkan masuk lagi. Kalau id-nya sudah pernah terlihat, kembalikan 200 dan berhenti — pengiriman pertama sudah mengerjakan tugasnya.
Logika dedupe ini semestinya ada di layer yang sama dengan yang memegang versioning API dan kontrak integrasi: ini bagian dari kontrak, bukan tempelan.
Yang terjadi di sisi pengirim
Semua di atas berlaku saat Anda jadi penerima webhook. Kalau sistem Anda yang jadi pengirim — misalnya memberi tahu sistem partner saat booking clear — desain retry-nya jadi tanggung jawab Anda sendiri. Dua ujung ekstremnya bisa jadi patokan.
Stripe melakukan retry dengan exponential backoff sampai tiga hari di live mode — jeda antar-percobaannya makin lama — sambil meregenerasi signature dan timestamp di setiap percobaan. Shopify melakukan retry sampai 8 kali dalam 4 jam. Setelah 8 kegagalan berturut-turut, Shopify otomatis menghapus webhook subscription yang dibuat lewat Admin API-nya dan mengirim peringatan ke kontak developer darurat app itu. Kedua provider memberi endpoint yang bermasalah waktu berjam-jam sampai berhari-hari untuk pulih, lalu berhenti dan membuat kegagalannya terlihat, bukan retry selamanya.
GitHub sengaja mengambil posisi sebaliknya. GitHub tidak otomatis mengirim ulang webhook yang gagal. Dokumennya menyatakan ini secara langsung dan hanya menawarkan redelivery manual lewat UI, atau script buatan sendiri yang polling untuk mencari dan mengirim ulang delivery yang gagal. Ujung yang satu ini jadi peringatan bagi siapa pun yang membangun webhook sender sendiri.
Tanpa jadwal backoff yang disengaja dan daftar dead-letter — tampungan kiriman yang gagal terus sampai jatah retry-nya habis — satu delivery yang gagal hilang begitu saja. Tidak ada yang tahu sampai pelanggan bertanya ke mana konfirmasinya.
Bangun tabel dead-letter untuk apa pun yang menghabiskan jatah retry — bukan satu baris log yang terkubur di aggregator, tapi daftar yang bisa di-query dan di-replay operator.
Rekonsiliasi adalah jaring pengaman, bukan rencana utama
Sekalipun sudah ada signature, respons cepat, dedupe, dan retry yang solid, webhook saja bukan jaminan yang tahan lama. Jaringan bisa kena partition, endpoint bisa di-redeploy persis saat pengiriman berlangsung, dan queue bisa menumpuk lebih lama dari rentang retry provider. Stripe, provider dengan desain retry paling baik di antara ketiganya, tetap menyarankan untuk tidak mengandalkan webhook sebagai satu-satunya sumber kebenaran.
Pola rekonsiliasi resminya adalah polling List Events API dengan delivery_success=false untuk menangkap apa pun yang gagal terkirim. Catatannya, API ini hanya mengembalikan event dari 30 hari terakhir. Bahkan jaring pengaman ini punya batas waktu.
Versi praktis untuk kebanyakan tim: job yang jalan tiap malam, mengambil daftar charge atau booking terbaru dari API provider dan membandingkannya dengan yang tercatat di sistem sendiri. Apa pun yang ada di sisi provider tapi hilang di sistem sendiri ditandai dan di-replay manual. Ini persis disiplin operasional yang menjaga kanal seperti konfirmasi booking lewat WhatsApp tetap bisa dipercaya. Pesan baru dikirim setelah event yang mendasarinya benar-benar sampai dan diproses, bukan sekadar karena webhook-nya terpicu.
Empat hal yang menjaga uang tetap beres
- Verifikasi signature terhadap raw body yang belum di-parse, dengan perbandingan constant-time dan batas toleransi timestamp.
- Idempotency key atau dedupe event id di setiap handler, agar retry tidak pernah memproses dua kali.
- Dashboard dead-letter untuk apa pun yang menghabiskan jatah retry-nya, bukan hilang begitu saja tanpa jejak.
- Job rekonsiliasi tiap malam yang memperlakukan API provider sebagai sumber kebenaran dan log webhook sebagai cache yang mungkin punya celah.
Tidak satu pun dari empat hal ini proyek besar. Masing-masing cuma butuh beberapa jam disiplin engineering, tepat di titik yang biasanya dipotong demi cepat selesai. Lewatkan, dan ongkosnya datang belakangan — jauh lebih mahal dari beberapa jam itu. Ongkos itu muncul entah di antrean support, di buku besar yang tidak cocok, atau di refund yang tidak bisa dijelaskan siapa pun.
Artikel Terkait
Mau bangun hal serupa?
Jasa Pengembangan Backend IoT & Integrasi Multi-Protokol
Backend yang menyerap telemetri perangkat lintas MQTT, WebSocket, Modbus, dan BLE, lalu menyatukannya jadi dashboard real-time yang andal.
Lihat cara saya bisa bantu