Pelajari cara membuat integrasi webhook yang andal dengan penandatanganan, kunci idempotensi, proteksi replay, dan alur debugging cepat untuk kegagalan yang dilaporkan pelanggan.

Ketika seseorang bilang “webhook rusak”, biasanya mereka maksudkan salah satu dari tiga hal: event tidak pernah tiba, event datang dua kali, atau event tiba dalam urutan yang membingungkan. Dari sudut pandang mereka, sistem “melewatkan” sesuatu. Dari sudut pandang Anda, provider memang mengirimkannya, tetapi endpoint Anda tidak menerima, tidak memproses, atau tidak mencatatnya seperti yang Anda harapkan.
Webhooks hidup di internet publik. Request bisa tertunda, di-retry, dan kadang datang tidak berurutan. Sebagian besar provider melakukan retry agresif saat melihat timeout atau respons non-2xx. Itu mengubah gangguan kecil (database lambat, deploy, pemadaman singkat) menjadi duplikasi dan kondisi perlombaan.
Log yang buruk membuat ini terasa acak. Jika Anda tidak bisa membuktikan apakah sebuah request autentik, Anda tidak bisa bertindak aman. Jika Anda tidak bisa mengaitkan keluhan pelanggan ke percobaan pengiriman tertentu, Anda akan menebak-nebak.
Kebanyakan kegagalan dunia nyata masuk ke beberapa kategori:
Tujuan praktisnya sederhana: terima event asli satu kali, tolak yang palsu, dan tinggalkan jejak yang jelas supaya Anda bisa debugging laporan pelanggan dalam hitungan menit.
Webhook hanyalah request HTTP yang dikirim provider ke endpoint yang Anda buka. Anda tidak mem-pull seperti panggilan API. Pengirim mendorongnya ketika sesuatu terjadi, dan tugas Anda adalah menerimanya, merespons dengan cepat, dan memprosesnya dengan aman.
Pengantaran biasanya mencakup body request (sering JSON) plus header yang membantu Anda memvalidasi dan melacak apa yang diterima. Banyak provider menyertakan timestamp, tipe event (seperti invoice.paid), dan ID event unik yang bisa Anda simpan untuk mendeteksi duplikasi.
Yang mengejutkan tim: pengantaran hampir tidak pernah “tepat sekali.” Kebanyakan provider menargetkan “at least once,” yang berarti event yang sama bisa datang beberapa kali, kadang berjarak menit atau jam.
Retry terjadi karena alasan biasa: server Anda lambat atau timeout, Anda mengembalikan 500, jaringan mereka tidak melihat 200 Anda, atau endpoint Anda sempat tidak tersedia saat deploy atau lonjakan trafik.
Timeout terutama berbahaya. Server Anda mungkin menerima request dan bahkan menyelesaikan pemrosesan, tetapi respons tidak sampai ke pengirim tepat waktu. Dari sudut pandang provider itu gagal, jadi mereka retry. Tanpa perlindungan, Anda memproses event yang sama dua kali.
Model mental yang bagus adalah memperlakukan request HTTP sebagai “percobaan pengiriman,” bukan “event.” Event diidentifikasi oleh ID-nya. Pemrosesan Anda harus berdasarkan ID itu, bukan berapa kali provider memanggil Anda.
Penandatanganan webhook adalah cara pengirim membuktikan sebuah request benar-benar dari mereka dan tidak diubah dalam perjalanan. Tanpa penandatanganan, siapa pun yang menebak URL webhook Anda bisa mengirim event palsu seperti “pembayaran berhasil” atau “user naik level”. Lebih buruk lagi, event asli bisa diubah di tengah jalan (jumlah, ID pelanggan, tipe event) dan tetap tampak valid bagi aplikasi Anda.
Pola yang paling umum adalah HMAC dengan secret yang dibagi. Kedua pihak mengetahui nilai secret yang sama. Pengirim mengambil payload webhook persis (biasanya raw request body), menghitung HMAC menggunakan secret itu, dan mengirimkan signature bersama payload. Tugas Anda adalah menghitung ulang HMAC di atas bytes yang sama dan memeriksa apakah signature cocok.
Data signature biasanya ditempatkan dalam header HTTP. Beberapa provider juga menyertakan timestamp di situ sehingga Anda bisa menambahkan proteksi replay. Jarang terjadi, signature disematkan dalam body JSON, yang lebih berisiko karena parser atau re-serialisasi bisa mengubah format dan memecah verifikasi.
Saat membandingkan signature, jangan gunakan pemeriksaan string biasa. Perbandingan dasar bisa membocorkan perbedaan waktu yang membantu penyerang menebak signature yang benar lewat banyak percobaan. Gunakan fungsi perbandingan waktu-konstan dari bahasa atau library kripto Anda, dan tolak jika ada mismatch.
Jika pelanggan melaporkan “sistem Anda menerima event yang tidak kami kirim,” mulailah dengan pemeriksaan signature. Jika verifikasi signature gagal, kemungkinan Anda memiliki mismatch secret atau Anda meng-hash bytes yang salah (misalnya JSON yang sudah diparse bukan raw body). Jika lulus, Anda bisa mempercayai identitas pengirim dan lanjut ke dedupe, urutan, dan retry.
Penanganan webhook yang andal dimulai dari satu aturan membosankan: verifikasi apa yang Anda terima, bukan yang Anda harapkan.
Tangkap raw request body persis seperti saat tiba. Jangan parse dan re-serialize JSON sebelum mengecek signature. Perbedaan kecil (whitespace, urutan kunci, unicode) mengubah bytes dan bisa membuat signature yang valid terlihat tidak valid.
Kemudian bangun payload persis seperti yang provider harapkan Anda tandatangani. Banyak sistem menandatangani string seperti timestamp + "." + raw_body. Timestamp bukan sekadar hiasan. Itu ada agar Anda bisa menolak request lama.
Hitung HMAC menggunakan secret bersama dan hash yang diperlukan (sering SHA-256). Simpan secret di tempat aman dan perlakukan seperti password.
Terakhir, bandingkan nilai yang Anda hitung dengan header signature menggunakan perbandingan waktu-konstan. Jika tidak cocok, kembalikan 4xx dan hentikan. Jangan “menerima tetap.”
Checklist implementasi singkat:
Seorang pelanggan melaporkan “webhook berhenti bekerja” setelah Anda menambahkan middleware parsing JSON. Anda melihat mismatch signature, kebanyakan pada payload besar. Perbaikan biasanya adalah memverifikasi menggunakan raw body sebelum parsing apa pun, dan mencatat langkah mana yang gagal (misalnya, “header signature hilang” vs “timestamp di luar jendela yang diizinkan”). Detail kecil itu sering memangkas waktu debugging dari jam ke menit.
Provider melakukan retry karena pengantaran tidak dijamin. Server Anda mungkin mati selama satu menit, sebuah hop jaringan bisa menjatuhkan request, atau handler Anda bisa timeout. Provider mengasumsikan “mungkin berhasil” dan mengirim event yang sama lagi.
Kunci idempotensi adalah nomor resi yang Anda gunakan untuk mengenali event yang sudah diproses. Ini bukan fitur keamanan, dan bukan pengganti verifikasi signature. Ini juga tidak akan memperbaiki race condition kecuali Anda menyimpannya dan memeriksanya dengan aman di bawah konkurensi.
Memilih kunci tergantung pada apa yang diberikan provider. Utamakan nilai yang tetap stabil di seluruh retry:
Saat menerima webhook, tulis kunci ke penyimpanan terlebih dahulu menggunakan aturan keunikan sehingga hanya satu request “menang.” Lalu proses event. Jika Anda melihat kunci yang sama lagi, kembalikan sukses tanpa mengerjakan ulang.
Simpan “resi” yang ringkas namun berguna: kunci, status pemrosesan (received/processed/failed), timestamp (first seen/last seen), dan ringkasan minimal (tipe event dan ID objek terkait). Banyak tim menyimpan kunci selama 7 sampai 30 hari agar retry terlambat dan laporan pelanggan tertutup.
Proteksi replay menghentikan masalah sederhana tapi berbahaya: seseorang menangkap request webhook nyata (dengan signature valid) dan mengirimkannya lagi nanti. Jika handler Anda memperlakukan setiap pengiriman sebagai baru, replay itu bisa memicu refund ganda, undangan pengguna terduplikasi, atau perubahan status berulang.
Pendekatan umum adalah menandatangani tidak hanya payload tetapi juga timestamp. Webhook Anda menyertakan header seperti X-Signature dan X-Timestamp. Saat diterima, verifikasi signature dan juga verifikasi bahwa timestamp masih segar di dalam jendela singkat.
Clock drift biasanya yang menyebabkan penolakan palsu. Server Anda dan server pengirim bisa berbeda beberapa menit, dan jaringan bisa menunda pengiriman. Beri buffer dan catat alasan penolakan.
Aturan praktis yang bekerja baik:
abs(now - timestamp) <= window (misalnya 5 menit plus sedikit grace).Jika timestamp hilang, Anda tidak bisa melakukan proteksi replay berbasis waktu sejati. Dalam kasus itu, andalkan idempotensi lebih kuat (simpan dan tolak event ID duplikat) dan pertimbangkan untuk meminta timestamp di versi webhook berikutnya.
Rotasi secret juga penting. Jika Anda merotasi signing secret, pertahankan beberapa secret aktif untuk periode overlap singkat. Verifikasi terhadap secret terbaru dulu, lalu fallback ke yang lebih lama. Ini menghindari gangguan pelanggan saat rollout. Jika tim Anda cepat mengirim endpoint (misalnya menghasilkan kode dengan Koder.ai dan menggunakan snapshot serta rollback saat deploy), jendela overlap itu membantu karena versi lama mungkin masih hidup sebentar.
Retry adalah normal. Anggap setiap pengiriman mungkin diduplikasi, tertunda, atau tidak berurutan. Handler Anda harus berperilaku sama apakah melihat event satu kali atau lima kali.
Pendekkan jalur request. Lakukan hanya yang wajib untuk menerima event, lalu pindahkan pekerjaan berat ke job latar.
Pola sederhana yang tahan di produksi:
Kembalikan 2xx hanya setelah Anda memverifikasi signature dan mencatat event (atau mengantrikannya). Jika Anda mengembalikan 200 sebelum menyimpan apa pun, Anda bisa kehilangan event saat crash. Jika Anda melakukan pekerjaan berat sebelum merespons, timeout memicu retry dan Anda mungkin mengulang efek samping.
Sistem hilir yang lambat adalah alasan utama mengapa retry menyakitkan. Jika penyedia email, CRM, atau database Anda lambat, biarkan queue menyerap penundaan. Worker dapat retry dengan backoff, dan Anda bisa memberi alert pada job yang macet tanpa memblokir pengirim.
Event yang datang tidak berurutan juga terjadi. Misalnya, sebuah subscription.updated bisa datang sebelum subscription.created. Bangun toleransi dengan memeriksa state saat ini sebelum menerapkan perubahan, mengizinkan upsert, dan memperlakukan “tidak ditemukan” sebagai alasan untuk retry nanti (saat itu masuk akal) daripada kegagalan permanen.
Banyak masalah webhook yang tampak “acak” sebenarnya self-inflicted. Mereka terlihat seperti jaringan yang flakey, tetapi berulang dalam pola, biasanya setelah deploy, rotasi secret, atau perubahan parsing kecil.
Bug signature paling umum adalah meng-hash bytes yang salah. Jika Anda parse JSON terlebih dulu, server Anda bisa memformat ulang (whitespace, urutan kunci, format angka). Lalu Anda memverifikasi signature terhadap body yang berbeda dari yang ditandatangani pengirim, dan verifikasi gagal meskipun payload asli. Selalu verifikasi terhadap raw request body bytes persis seperti diterima.
Sumber kebingungan besar berikutnya adalah secret. Tim melakukan pengujian di staging tapi tidak sengaja memverifikasi dengan secret produksi, atau menyimpan secret lama setelah rotasi. Ketika pelanggan melaporkan kegagalan “hanya di satu environment,” asumsikan secret salah atau konfigurasi yang salah terlebih dulu.
Beberapa kesalahan yang memunculkan investigasi panjang:
Contoh: pelanggan mengatakan “order.paid tidak pernah tiba.” Anda melihat kegagalan signature mulai setelah refactor yang mengganti middleware parsing request. Middleware itu membaca dan meng-encode ulang JSON, sehingga pengecekan signature kini menggunakan body yang dimodifikasi. Perbaikan sederhana, tetapi hanya jika Anda tahu untuk mencarinya.
Saat pelanggan mengatakan “webhook Anda tidak memicu,” perlakukan itu seperti masalah trace, bukan tebak-tebakan. Berpegang pada satu percobaan pengiriman yang tepat dari provider dan ikuti melalui sistem Anda.
Mulai dengan mendapatkan identifier pengiriman provider, request ID, atau event ID untuk percobaan yang gagal. Dengan satu nilai itu, Anda harusnya bisa menemukan entri log yang cocok dengan cepat.
Dari sana, periksa tiga hal berurutan:
Lalu konfirmasi apa yang Anda kembalikan ke provider. 200 yang lambat bisa sama buruknya dengan 500 jika provider timeout dan retry. Lihat kode status, waktu respons, dan apakah handler Anda mengakui sebelum melakukan pekerjaan berat.
Jika perlu mereproduksi, lakukan dengan aman: simpan sampel request raw yang sudah direduksi (header kunci plus raw body) dan replay di lingkungan test menggunakan secret dan kode verifikasi yang sama.
Saat integrasi webhook mulai gagal “acak”, kecepatan lebih penting daripada kesempurnaan. Runbook ini menangkap penyebab biasa.
Ambil satu contoh konkret terlebih dulu: nama provider, tipe event, perkiraan timestamp (dengan zona waktu), dan event ID yang bisa dilihat pelanggan.
Lalu verifikasi:
Jika provider bilang “kami me-retry 20 kali,” periksa pola umum dulu: secret salah (signature gagal), drift jam (jendela replay), batas ukuran payload (413), timeout (tanpa respons), dan lonjakan 5xx dari dependensi hilir.
Seorang pelanggan mengirim email: “Kami kehilangan event invoice.paid kemarin. Sistem kami tidak pernah memperbarui.” Berikut cara cepat menelusurinya.
Pertama, konfirmasi apakah provider mencoba mengirim. Ambil event ID, timestamp, URL tujuan, dan kode respons tepat yang dikembalikan endpoint Anda. Jika ada retry, catat alasan kegagalan pertama dan apakah retry berikutnya berhasil.
Selanjutnya, validasi apa yang kode Anda lihat di edge: konfirmasi signing secret yang dikonfigurasikan untuk endpoint itu, hitung ulang verifikasi signature menggunakan raw request body, dan cek timestamp request terhadap jendela yang Anda izinkan.
Hati-hati dengan jendela replay saat retry. Jika jendela Anda 5 menit dan provider me-retry 30 menit kemudian, Anda mungkin menolak retry yang sah. Jika itu kebijakan Anda, pastikan itu disengaja dan terdokumentasi. Jika tidak, perluas jendela atau ubah logika sehingga idempotensi tetap menjadi pertahanan utama terhadap duplikasi.
Jika signature dan timestamp tampak baik, ikuti event ID melalui sistem Anda dan jawab: apakah Anda memprosesnya, mendedupe-nya, atau membuangnya?
Hasil umum:
Saat membalas pelanggan, buat jawaban ringkas dan spesifik: “Kami menerima percobaan pada 10:03 dan 10:33 UTC. Yang pertama timeout setelah 10s; retry ditolak karena timestamp berada di luar jendela 5 menit kami. Kami memperlebar jendela dan menambahkan acknowledgement yang lebih cepat. Mohon kirim ulang event ID X jika diperlukan.”
Cara tercepat menghentikan masalah webhook adalah membuat setiap integrasi mengikuti playbook yang sama. Tuliskan kontrak yang Anda dan pengirim sepakati: header wajib, metode penandatanganan tepat, timestamp mana yang digunakan, dan ID mana yang Anda anggap unik.
Standarkan apa yang Anda catat untuk setiap percobaan pengiriman. Log receipt kecil biasanya cukup: received_at, event_id, delivery_id, signature_valid, idempotency_result (new/duplicate), handler_version, dan response status.
Alur kerja yang tetap berguna seiring pertumbuhan:
Jika Anda membangun aplikasi di Koder.ai (koder.ai), Planning Mode adalah cara yang bagus untuk mendefinisikan kontrak webhook terlebih dulu (header, penandatanganan, ID, perilaku retry) lalu menghasilkan endpoint dan record receipt yang konsisten di seluruh proyek. Konsistensi itulah yang membuat debugging cepat, bukan heroik.
Karena pengiriman webhook biasanya at-least-once, bukan exactly-once. Provider melakukan retry saat terjadi timeout, respons 5xx, atau saat mereka tidak melihat 2xx Anda tepat waktu, sehingga Anda bisa mendapatkan duplikasi, penundaan, dan pengiriman yang tidak berurutan meskipun sistem terlihat “berfungsi”.
Aturan dasar: verifikasi tanda tangan dulu, lalu simpan/dedupe event, lalu kembalikan 2xx, lalu kerjakan tugas berat secara asinkron.
Jika Anda melakukan tugas berat sebelum membalas, Anda akan terkena timeout dan memicu retry; jika Anda membalas sebelum mencatat apa pun, Anda bisa kehilangan event jika terjadi crash.
Gunakan raw request body bytes persis seperti yang diterima. Jangan parse JSON dan re-serialize sebelum verifikasi—whitespace, urutan kunci, dan format angka bisa mengubah bytes dan merusak signature.
Juga pastikan Anda merekonstruksi string yang ditandatangani provider secara tepat (sering kali timestamp + "." + raw_body).
Kembalikan 4xx (umumnya 400 atau 401) dan jangan proses payload tersebut.
Catat alasan minimal (header signature hilang, mismatch, jendela timestamp buruk), tetapi jangan mencatat secret atau payload sensitif secara penuh.
Kunci idempotensi adalah identifier unik yang stabil yang Anda simpan agar retry tidak menerapkan efek samping dua kali.
Pilihan terbaik:
Terapkan dengan sehingga hanya satu request yang “menang” saat konkurensi.
Tulis kunci idempotensi sebelum melakukan efek samping, dengan aturan keunikan. Lalu:
Jika insert gagal karena kunci sudah ada, kembalikan 2xx dan lewati aksi bisnis.
Gunakan timestamp yang disertakan dalam data yang ditandatangani dan tolak request yang berada di luar jendela waktu singkat (misalnya beberapa menit).
Untuk menghindari menolak retry yang sah:
Jangan menganggap urutan pengiriman sama dengan urutan event. Buat handler toleran:
Simpan event ID dan tipe agar Anda bisa menelusuri kejadian saat urutan berantakan.
Catat receipt kecil per percobaan pengiriman supaya Anda bisa menelusuri satu event ujung-ke-ujung:
Buat log dapat dicari berdasarkan event ID agar support bisa menjawab laporan pelanggan dengan cepat.
Minta satu identifier konkret: event ID atau delivery ID, plus timestamp perkiraan.
Lalu periksa urutannya:
Jika Anda membangun endpoint dengan Koder.ai, pertahankan pola handler konsisten (verify → record/dedupe → queue → respond). Konsistensi membuat pemeriksaan insiden jadi cepat.