Audit kontrak webhook perlu dilakukan dari sisi desain, bukan hanya implementasi handler. Jika penyedia webhook melakukan retry saat timeout atau menerima status non-2xx, sistem penerima yang tidak idempoten dapat membuat order ganda, mengirim email berulang, atau menulis status yang mundur karena event datang tidak berurutan.
Untuk tim backend dan integrasi, target auditnya sederhana: setiap webhook harus bisa diverifikasi, diproses aman saat duplikat, tetap benar saat urutan event berubah, dan bisa ditelusuri saat gagal. Dalam konteks praktik rekayasa yang makin menuntut jejak audit dan pengendalian risiko—termasuk dorongan dari CRA Uni Eropa terhadap rekayasa yang lebih dapat diaudit dan aman—kontrak webhook yang eksplisit lebih penting daripada asumsi implisit di dokumentasi atau kode integrasi.
Apa yang harus diaudit pada kontrak webhook
Audit tidak berhenti pada pertanyaan “apakah webhook terkirim?”. Fokus utamanya adalah apakah kontrak antar sistem cukup jelas untuk menangani kondisi dunia nyata:
- Retry: pengirim bisa mengirim event yang sama berkali-kali.
- Duplikasi: event identik bisa muncul lebih dari sekali, bahkan di jalur distribusi yang berbeda.
- Out-of-order: event status terbaru belum tentu datang terakhir.
- Verifikasi gagal: signature salah, timestamp kedaluwarsa, atau body berubah di proxy.
- Kegagalan parsial: event tersimpan tetapi side effect gagal, atau sebaliknya.
Jika kontrak webhook tidak mendefinisikan hal-hal ini secara eksplisit, integrator akan menebak perilakunya. Tebakan yang berbeda antar tim adalah sumber bug yang paling mahal untuk dilacak.
Komponen kontrak webhook yang wajib eksplisit
1. Signature header untuk autentikasi dan integritas
Webhook sebaiknya tidak hanya mengandalkan IP allowlist atau secret di URL. Kontrak perlu mendefinisikan header signature, algoritma yang dipakai, elemen yang ditandatangani, dan cara verifikasinya.
Minimal, dokumentasikan:
- Nama header, misalnya
X-Signatureatau pola setara. - Apakah signature dihitung dari raw request body atau payload yang sudah diparse.
- Apakah timestamp ikut ditandatangani.
- Format header, misalnya pasangan
t=timestampdanv1=signature. - Prosedur rotasi secret dan dukungan secret aktif ganda selama masa transisi.
Mengapa ini penting: banyak kegagalan verifikasi terjadi bukan karena secret salah, tetapi karena penerima menghitung HMAC dari JSON yang sudah di-format ulang. Signature harus dihitung dari raw bytes yang sama dengan yang dikirim pengirim.
// Contoh pseudocode verifikasi signature webhook berbasis HMAC-SHA256
rawBody = getRawRequestBody()
timestamp = request.header("X-Webhook-Timestamp")
signature = request.header("X-Webhook-Signature")
if abs(nowUnix() - parseInt(timestamp)) > 300:
return 400 // timestamp di luar toleransi
signedPayload = timestamp + "." + rawBody
expected = hmacSha256Hex(secret, signedPayload)
if !constantTimeEquals(expected, signature):
return 401 // signature invalid
// lanjutkan ke tahap deduplikasi dan enqueueCatatan audit: jika dokumentasi hanya menulis “gunakan signature” tanpa menjelaskan sumber data untuk hashing, itu belum cukup aman untuk integrasi lintas bahasa dan framework.
2. Event ID yang stabil dan unik
Setiap webhook harus membawa event ID yang unik dan stabil untuk satu kejadian logis. Event ID inilah yang dipakai penerima untuk mendeteksi duplikasi lintas retry.
Audit kontrak dengan pertanyaan berikut:
- Apakah event ID selalu ada?
- Apakah retry untuk event yang sama mempertahankan event ID yang sama?
- Apakah event ID unik secara global atau minimal unik per tenant/source?
- Apakah event ID ada di header, body, atau keduanya?
Mengapa ini penting: tanpa event ID, penerima sering memakai hash body sebagai pengganti. Itu rapuh, karena perubahan kecil seperti urutan field JSON atau metadata tambahan bisa menghasilkan hash berbeda untuk event yang sebenarnya sama.
3. Idempotency key untuk aksi yang memicu side effect
Event ID dan idempotency key tidak selalu sama fungsinya. Event ID mencegah pemrosesan ganda dari event yang identik. Idempotency key dipakai untuk memastikan satu aksi bisnis hanya dieksekusi sekali walau dipicu dari beberapa jalur.
Contoh: event payment.captured dan pemanggilan API internal bisa sama-sama memicu pembuatan invoice. Jika keduanya tidak dikaitkan dengan idempotency key yang sama, side effect masih bisa terduplikasi walau webhook sudah dideduplikasi.
Pola yang lebih aman:
- Simpan
event_iduntuk deduplikasi penerimaan webhook. - Simpan
idempotency_keyuntuk side effect bisnis seperti membuat invoice, mengubah stok, atau mengirim notifikasi. - Gunakan constraint unik di database pada kombinasi kunci yang relevan.
-- Contoh skema deduplikasi dan idempotensi
CREATE TABLE webhook_receipts (
source_system VARCHAR(100) NOT NULL,
event_id VARCHAR(200) NOT NULL,
received_at TIMESTAMP NOT NULL,
verification_status VARCHAR(20) NOT NULL,
payload_json TEXT NOT NULL,
PRIMARY KEY (source_system, event_id)
);
CREATE TABLE invoice_commands (
tenant_id VARCHAR(100) NOT NULL,
idempotency_key VARCHAR(200) NOT NULL,
created_at TIMESTAMP NOT NULL,
status VARCHAR(20) NOT NULL,
PRIMARY KEY (tenant_id, idempotency_key)
);4. Timestamp tolerance untuk mencegah replay
Kontrak webhook sebaiknya mendefinisikan timestamp dan toleransi waktu yang diterima, misalnya beberapa menit. Tujuannya bukan presisi waktu bisnis, tetapi untuk membatasi replay attack dan salah proses dari request lama yang dikirim ulang di luar konteks.
Trade-off-nya:
- Toleransi terlalu sempit: rawan false negative jika ada clock skew atau antrean jaringan.
- Toleransi terlalu longgar: memperlebar jendela replay.
Audit penerima juga perlu mengecek apakah server melakukan sinkronisasi waktu dengan benar. Toleransi 5 menit tidak berguna jika jam mesin melenceng 8 menit.
5. Status code yang mengendalikan retry dengan jelas
Banyak integrasi rusak karena pengirim dan penerima menafsirkan status code secara berbeda. Kontrak webhook harus menjelaskan kapan pengirim akan melakukan retry dan respons apa yang dianggap final.
Panduan praktis yang umum dipakai:
- 2xx: event diterima; pengirim tidak perlu retry.
- 4xx: umumnya dianggap kesalahan permanen di request, tetapi harus jelas apakah pengirim tetap retry untuk beberapa kode tertentu.
- 5xx: kesalahan sementara; pengirim boleh retry.
Pada sisi penerima, pola aman biasanya:
- Verifikasi signature dan timestamp.
- Cek event ID di store deduplikasi.
- Simpan receipt secara atomik.
- Segera kembalikan 2xx setelah event berhasil diterima untuk diproses async, bukan setelah semua side effect selesai.
Mengapa ini bekerja: memisahkan tahap penerimaan dari pemrosesan business logic mengurangi timeout dan retry yang tidak perlu. Jika side effect lama, antrekan pekerjaan ke queue internal.
Jangan mengembalikan 500 hanya karena pengiriman email internal gagal setelah event tervalidasi dan tersimpan. Itu akan memancing retry webhook dan memperbesar peluang aksi ganda.
6. Retry policy yang terdokumentasi
Kontrak yang baik tidak hanya berkata “kami akan retry”, tetapi juga menjelaskan garis besar kebijakannya:
- pemicu retry: timeout, koneksi gagal, status code tertentu;
- apakah menggunakan exponential backoff;
- berapa lama event dapat dicoba ulang sebelum gagal permanen;
- apakah ada batas jumlah percobaan;
- apakah ada dead-letter atau dashboard retry.
Anda tidak harus membuka semua detail operasional internal, tetapi integrator perlu tahu apakah event bisa datang lagi dalam hitungan detik, menit, atau hari. Ini mempengaruhi TTL deduplikasi, retention log, dan ekspektasi support.
7. Dead-letter dan proses re-drive
Jika webhook gagal terus, sistem pengirim yang matang biasanya menempatkan event ke dead-letter queue atau status gagal final. Dari sudut audit kontrak, penting untuk menanyakan:
- Apakah event gagal bisa dilihat operator?
- Apakah ada mekanisme re-drive atau kirim ulang manual?
- Apakah re-drive mempertahankan event ID asli?
- Apakah payload yang dikirim ulang identik atau hasil regenerasi terbaru?
Mengapa ini penting: kirim ulang manual tanpa mempertahankan identitas event sering menjadi sumber duplikasi yang lolos dari sistem deduplikasi penerima.
8. Versioning payload
Webhook adalah kontrak jangka panjang. Payload harus memiliki strategi versioning yang jelas, misalnya versi skema di header atau field eksplisit dalam body.
Audit dengan pertanyaan:
- Apakah penambahan field baru dijamin kompatibel ke belakang?
- Apakah field bisa hilang atau berubah tipe?
- Apakah event type juga memiliki versi?
- Bagaimana masa transisi saat skema baru dirilis?
Prinsip aman: penerima sebaiknya toleran terhadap field tambahan, tetapi ketat pada field kritis yang wajib ada. Pengirim sebaiknya menghindari perubahan makna field secara diam-diam.
{
"event_id": "evt_01HZY...",
"event_type": "invoice.paid",
"schema_version": "2025-01",
"occurred_at": "2025-01-10T12:34:56Z",
"resource_id": "inv_12345",
"tenant_id": "tenant_abc",
"idempotency_key": "invoice-paid:inv_12345",
"data": {
"invoice_id": "inv_12345",
"amount": 125000,
"currency": "EUR"
}
}Pola pemrosesan yang aman di sisi penerima
Alur minimum yang direkomendasikan
- Ambil raw body tanpa modifikasi.
- Verifikasi signature dan timestamp.
- Validasi field kontrak minimum: event ID, event type, occurred_at, schema version.
- Lakukan insert receipt dengan constraint unik pada
source_system + event_id. - Jika insert gagal karena duplikat, kembalikan 2xx dan hentikan.
- Enqueue pekerjaan async menggunakan event ID sebagai referensi utama.
- Di worker, terapkan idempotency untuk side effect bisnis.
- Simpan hasil pemrosesan dan korelasikan dengan event ID.
Poin kuncinya adalah deduplikasi lebih awal dan idempotensi lebih dalam. Deduplikasi di gateway masuk tidak cukup jika worker masih bisa membuat side effect ganda saat retry internal terjadi.
Contoh pseudocode handler
function handleWebhook(request):
rawBody = request.rawBody
headers = request.headers
verifySignatureOrFail(headers, rawBody)
verifyTimestampOrFail(headers)
payload = parseJson(rawBody)
validateRequiredFields(payload)
inserted = insertWebhookReceiptIfAbsent(
sourceSystem = "billing-provider",
eventId = payload.event_id,
payloadJson = rawBody,
verificationStatus = "verified"
)
if !inserted:
return response(200, "duplicate ignored")
enqueue("process_webhook_event", {
source_system: "billing-provider",
event_id: payload.event_id
})
return response(202, "accepted")Jika pengirim mensyaratkan 200 OK alih-alih 202 Accepted, sesuaikan. Yang penting adalah dokumentasi kedua pihak sepakat bahwa respons tersebut berarti event sudah diterima dan tidak perlu retry.
Menangani event out-of-order tanpa merusak state
Masalah umum lain adalah event datang tidak berurutan. Misalnya order.completed tiba lebih dulu, lalu order.processing datang belakangan akibat retry dari jalur lama. Jika sistem hanya melakukan last write wins berdasarkan waktu terima, state bisa mundur.
Pendekatan yang lebih aman
- Gunakan versi/sequence number jika disediakan pengirim.
- Gunakan occurred_at hanya jika Anda yakin jam pengirim konsisten dan semantics-nya jelas.
- Terapkan aturan transisi state yang eksplisit, misalnya status final tidak boleh ditimpa status sementara.
- Lakukan fetch state sumber bila event hanya notifikasi perubahan dan urutan sangat penting.
Contoh aturan bisnis: jika order sudah completed atau cancelled, event processing yang datang belakangan harus diabaikan atau ditandai anomali, bukan menurunkan state.
function applyOrderStatus(currentStatus, incomingStatus):
terminal = ["completed", "cancelled"]
if currentStatus in terminal and incomingStatus not in terminal:
return currentStatus // jangan mundur
return incomingStatusTrade-off: aturan lokal seperti ini sederhana, tetapi bisa keliru jika domain memiliki transisi yang lebih kompleks. Untuk domain kritis, state machine eksplisit lebih aman daripada kondisi ad hoc di dalam handler.
Anti-pattern yang sering ditemukan saat audit
1. Menggunakan payload hash sebagai satu-satunya deduplikasi
Ini gagal ketika metadata berubah atau urutan serialisasi JSON berbeda. Selalu prioritaskan event ID yang ditetapkan pengirim.
2. Memproses side effect sebelum menyimpan receipt
Jika proses mati setelah side effect berhasil tetapi sebelum event tercatat, retry berikutnya akan menjalankan aksi yang sama lagi.
3. Mengembalikan 500 untuk kesalahan downstream non-kritis
Misalnya invoice sudah tersimpan, tetapi notifikasi Slack gagal. Retry dari pengirim akan mengulang invoice jika idempotensi tidak ketat.
4. Menghitung signature dari JSON yang sudah diparse
Framework tertentu bisa mengubah whitespace, escaping, atau urutan field. Verifikasi harus memakai raw body.
5. Tidak punya TTL atau retention yang sesuai untuk store deduplikasi
Jika pengirim bisa retry selama beberapa hari tetapi dedupe store hanya menyimpan event selama beberapa jam, duplikasi terlambat akan lolos.
6. Mencampur event ID dan resource ID
Satu resource bisa memunculkan banyak event sah. Menjadikan invoice_id sebagai kunci deduplikasi webhook akan menelan event yang sebenarnya berbeda.
7. Mengandalkan urutan jaringan sebagai urutan bisnis
Urutan terima tidak sama dengan urutan kejadian. Ini terutama berbahaya jika ada beberapa worker, retry, atau failover.
Tabel keputusan singkat
| Masalah | Pilihan yang disarankan | Hindari jika |
|---|---|---|
| Autentikasi webhook | HMAC signature atas raw body + timestamp | Hanya mengandalkan IP allowlist atau secret di query string |
| Deduplikasi event | Unique key pada source_system + event_id | Hash payload sebagai satu-satunya identitas |
| Idempotensi side effect | Idempotency key terpisah + unique constraint | Menganggap deduplikasi receipt sudah cukup |
| Out-of-order event | Sequence/version atau aturan transisi state | Last write wins berdasarkan waktu terima |
| Respons endpoint | 2xx setelah receipt aman tersimpan dan di-enqueue | Menunggu semua proses downstream selesai sinkron |
| Event gagal permanen | Dead-letter + re-drive dengan identitas event tetap | Kirim ulang manual dengan event ID baru tanpa jejak |
| Evolusi payload | Schema version eksplisit dan kompatibilitas ke belakang | Perubahan tipe/arti field tanpa versi |
Observability yang wajib ada
Webhook yang aman tetapi tidak dapat diamati tetap sulit diaudit. Minimal, sistem harus menghasilkan jejak yang cukup untuk menjawab: “event ini diterima?”, “signature valid?”, “diproses berapa kali?”, dan “side effect mana yang dijalankan?”.
Field log dan metrik yang berguna
source_systemevent_idevent_typeschema_versiondelivery_attemptjika tersediaverification_statusdedupe_hitprocessing_statusidempotency_keyuntuk side effect utamacorrelation_idinternal untuk tracing antar service
Metrik yang layak dipantau:
- jumlah webhook masuk per sumber dan event type;
- rasio signature gagal;
- rasio duplikasi;
- latensi dari receipt ke selesai diproses;
- jumlah event di dead-letter;
- jumlah retry internal worker.
Tip debugging: saat ada dugaan duplikasi, periksa tiga lapis identitas sekaligus: event ID dari pengirim, idempotency key dari aksi bisnis, dan primary key rekaman internal. Banyak insiden terlihat seperti “webhook dobel”, padahal side effect digandakan oleh worker internal.
Checklist penerimaan integrasi webhook
Checklist berikut bisa dipakai saat onboarding provider baru atau review perubahan kontrak.
Checklist untuk tim integrasi/penerima
- Apakah endpoint bisa membaca raw request body?
- Apakah signature diverifikasi dengan perbandingan konstan waktu?
- Apakah timestamp diperiksa dengan toleransi yang terdokumentasi?
- Apakah event tanpa
event_idditolak? - Apakah receipt disimpan sebelum side effect dijalankan?
- Apakah ada unique constraint untuk deduplikasi event?
- Apakah worker side effect memakai idempotency key?
- Apakah event duplikat mengembalikan 2xx, bukan error?
- Apakah retention dedupe store cukup lebih lama dari jendela retry pengirim?
- Apakah ada aturan jelas untuk event out-of-order?
- Apakah field wajib dan schema version tervalidasi?
- Apakah log dan tracing menyimpan event ID dan status verifikasi?
- Apakah event gagal bisa dire-drive tanpa mengubah identitas event?
Checklist untuk reviewer API/penyedia webhook
- Nama header signature, timestamp, dan event ID terdokumentasi jelas.
- Algoritma signature dan bahan yang ditandatangani dijelaskan eksplisit.
- Dokumentasi menyebut raw body, bukan body JSON hasil parse.
- Perilaku retry dan makna status code dijelaskan.
- Event ID stabil lintas retry dan re-drive.
- Ada pembedaan jelas antara event ID, resource ID, dan idempotency key.
- Payload memiliki schema version atau strategi evolusi yang setara.
- Dokumentasi menyebut kemungkinan duplikasi dan out-of-order secara eksplisit.
- Ada panduan pengujian lokal, sandbox, atau contoh request yang realistis.
- Ada cara operator melihat delivery failure dan melakukan re-send dengan aman.
Contoh acceptance test yang layak diminta
Audit kontrak lebih kuat jika diterjemahkan menjadi acceptance test. Berikut skenario minimum:
- Retry identik: kirim request yang sama dua kali dengan event ID sama; side effect hanya terjadi sekali.
- Signature salah: ubah satu byte payload; endpoint menolak dan tidak menyimpan receipt sebagai valid.
- Timestamp kedaluwarsa: kirim event lama; endpoint menolak sesuai kebijakan toleransi.
- Out-of-order: kirim status final lalu status sementara; state tidak mundur.
- Downstream gagal: simulasikan kegagalan email/ERP; endpoint tetap tidak memicu duplikasi bisnis saat retry internal terjadi.
- Re-drive dead-letter: event gagal dikirim ulang; identitas event tetap dan deduplikasi masih bekerja.
Penutup
Audit kontrak webhook agar retry tidak memicu aksi ganda pada dasarnya adalah audit terhadap identitas, verifikasi, urutan, dan jejak operasi. Kontrak yang baik harus menjelaskan signature header, event ID, idempotency key, timestamp tolerance, status code, retry policy, dead-letter, versioning payload, dan observability secara eksplisit.
Untuk tim backend, implementasi yang paling aman biasanya mengikuti pola: verifikasi raw request, simpan receipt dengan unique constraint, akui penerimaan lebih cepat, proses async, lalu lindungi side effect dengan idempotency key. Dengan begitu, retry, duplikasi, dan event out-of-order menjadi kondisi yang dirancang untuk ditangani, bukan sumber insiden berulang yang baru terlihat saat sistem sudah ramai dipakai.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!