Menarik data dari API di chapter sebelumnya menempatkan kita sebagai pemohon: kita bertanya, layanan menjawab. Payment gateway menawarkan arah sebaliknya — begitu pembayaran berhasil, gateway langsung mengirim event ke alamat yang kita daftarkan. Mekanisme inilah yang disebut webhook: pola push di mana sistem kita berperan sebagai server penerima. Peran baru tersebut membawa tanggung jawab baru: kita harus memverifikasi bahwa pengirim benar-benar siapa yang diklaim, menangani pengiriman ulang yang tidak pernah diminta, dan menjaga endpoint tetap hidup. Chapter ini membahas ketiganya lewat handler mini yang diuji nyata di mesin lokal.
Konsep Push dan Endpoint Penerima #
Webhook adalah HTTP terbalik: alih-alih kita memanggil layanan lain, layanan lain yang memanggil endpoint milik kita — misalnya POST /webhook — setiap kali event terjadi. Konsekuensi pertamanya adalah ketersediaan: endpoint harus siap menerima permintaan kapan pun, karena event yang tidak tertangani hilang begitu gateway menyerah mencoba ulang. Konsekuensi keduanya adalah kecepatan: praktik terbaik menerima event, membalas 200 segera, lalu memproses isinya secara asinkron — pengirim tidak menunggu pipeline kita selesai.
Verifikasi Signature dengan HMAC #
Endpoint yang terbuka di internet bisa dikirim siapa saja, termasuk pihak yang menyamar sebagai gateway. Solusi standarnya adalah signature HMAC: kita dan gateway menyimpan rahasia bersama (shared secret); gateway menghitung HMAC-SHA256 atas isi permintaan, lalu menyertakan hasilnya di header. Penerima menghitung ulang dari isi yang diterima dan membandingkan kedua nilai. Pengirim palsu yang tidak memegang rahasia tidak mungkin menghasilkan signature yang cocok. Di sisi pengirim, perhitungannya sesederhana ini:
import hashlib
import hmac
rahasia = b"YOUR_WEBHOOK_SECRET"
body = b'{"id": "evt-001", "pelanggan": "Budi Santoso", "total": 150000}'
signature = hmac.new(rahasia, body, hashlib.sha256).hexdigest()
print(signature)Output yang Dihasilkan:
827a3c54d6e878dfb3bb0e469126cae3180550a124d88c07f0f2cafd3ac5b7a9Idempotency Key dan Perlindungan Replay #
Jaringan tidak bisa diandalkan, sehingga gateway mengirim ulang event sampai menerima jawaban 200 — event yang sama bisa tiba dua kali. Karena itu setiap event membawa id unik sebagai idempotency key: penerima mencatat kunci yang sudah diproses, dan pengiriman ulang dijawab 200 tanpa diproses lagi. Membalas 200 penting — balasan galat membuat gateway terus mencoba ulang selamanya.
Perlindungan replay melangkah lebih jauh: penyerang yang sempat menangkap permintaan sah bisa mengirim ulang salinannya. Penangkalnya adalah tanda waktu di header — penerima menolak event yang terlalu tua, misalnya lebih dari lima menit, karena pengiriman sah tidak pernah tertunda selama itu. Kombinasi signature, idempotency key, dan batas usia menutup tiga celah sekaligus.
Implementasi: Handler Webhook Mini #
Kita rakit seluruh konsep di atas: server http.server di thread menerima POST /webhook, memverifikasi HMAC, memeriksa idempotency, lalu memproses event — dan client tiruan menguji tiga skenario: pengiriman sah, pengiriman ulang, dan signature palsu:
import hashlib
import hmac
import json
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
import requests
RAHASIA = b"YOUR_WEBHOOK_SECRET"
EVENT_DIPROSES = set() # idempotency key yang sudah diterima
def hitung_signature(body: bytes) -> str:
return hmac.new(RAHASIA, body, hashlib.sha256).hexdigest()
class HandlerWebhook(BaseHTTPRequestHandler):
def do_POST(self):
panjang = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(panjang)
signature = self.headers.get("X-Signature", "")
if not hmac.compare_digest(signature, hitung_signature(body)):
print("DITOLAK : signature tidak cocok")
self.kirim_status(401)
return
event = json.loads(body)
if event["id"] in EVENT_DIPROSES:
print(f"DILOMPATI : event {event['id']} sudah pernah diproses")
self.kirim_status(200)
return
EVENT_DIPROSES.add(event["id"])
print(f"DIPROSES : event {event['id']} senilai {event['total']}")
self.kirim_status(200)
def kirim_status(self, kode):
self.send_response(kode)
self.send_header("Content-Length", "0")
self.end_headers()
def log_message(self, format, *args):
pass
server = HTTPServer(("127.0.0.1", 0), HandlerWebhook)
threading.Thread(target=server.serve_forever, daemon=True).start()
url = f"http://127.0.0.1:{server.server_address[1]}"
def kirim_event(id_event, total, signature_valid=True):
body = json.dumps(
{"id": id_event, "pelanggan": "Budi Santoso", "total": total}
).encode()
headers = {
"X-Signature": hitung_signature(body) if signature_valid else "palsu"
}
respons = requests.post(f"{url}/webhook", data=body, headers=headers, timeout=5)
print(f" client menerima jawaban {respons.status_code}")
try:
print("Pengiriman pertama (signature sah):")
kirim_event("evt-001", 150000)
print("Pengiriman ulang (idempotency key sama):")
kirim_event("evt-001", 150000)
print("Pengiriman dengan signature palsu:")
kirim_event("evt-002", 98000, signature_valid=False)
finally:
server.shutdown()
server.server_close()Output yang Dihasilkan:
Pengiriman pertama (signature sah):
DIPROSES : event evt-001 senilai 150000
client menerima jawaban 200
Pengiriman ulang (idempotency key sama):
DILOMPATI : event evt-001 sudah pernah diproses
client menerima jawaban 200
Pengiriman dengan signature palsu:
DITOLAK : signature tidak cocok
client menerima jawaban 401Mari kita telaah baris-baris kode di atas:
- Baris 10 (
EVENT_DIPROSES): himpunan kunci yang sudah diproses — di produksi berupa tabel database agar bertahan melebihi umur proses, tetapi himpunan cukup untuk demonstrasi. - Baris 23-26 (verifikasi):
compare_digestmencocokkan signature header dengan hitungan ulang atas isi mentah; kegagalan dijawab401dan isi tidak pernah diurai — permintaan palsu ditolak paling awal. - Baris 28-32 (idempotency):
event["id"]yang sudah tercatat dijawab200tanpa diproses — client melihat sukses, data tidak dobel. - Baris 34-36 (pemrosesan): hanya jalur ini yang benar-benar memproses event; kunci dicatat sebelum jawaban dikirim agar balasan
200selalu berarti “aman dikirim ulang sekali pun”. - Baris 52-60 (
kirim_event): client menghitung signature sendiri dari rahasia yang sama — inilah yang membuat tiruan berperilaku persis seperti gateway sungguhan, termasuk mode signature palsu untuk menguji jalur penolakan.
Tiga skenario di keluaran menegaskan alur lengkap: event sah diproses, pengiriman ulang dilompati, dan pemalsuan ditolak. Dokumentasi modulnya ada di hmac dan http.server .
Webhook menghubungkan dua sistem secara langsung — tetapi ketika satu pengirim harus diikuti banyak konsumen, atau event harus bertahan saat penerima sedang tidak siap, kita membutuhkan perantara yang menyimpan pesan di antrean. Peran itulah yang diisi message queue, dibahas di chapter Ingestion dari Message Queue.