Lewati ke konten utama
menjadi.dev
Chapter 39.4 API & Auth Lanjutan

Webhook & Idempotency: Jangan Charge User 2x

Memproses webhook payment dengan aman — idempotency, retry, dan idempotency key

Tujuan Pembelajaran

  • Memahami kenapa webhook payment BUTUH idempotency
  • Mampu mengimplementasikan idempotent webhook handler
  • Mampu menangani webhook retry dan duplicate events

Analogi

Diagram

      Webhook Payment = Pesan Delivery:

Kurir (PG Webhook): "Paket sudah sampai!" → Ketuk pintu 1x
Kamu: "Oke, saya update status jadi DELIVERED"

Masalah:
Kurir ketuk 3x (retry) tapi paket tetap 1 → status harus tetap DELIVERED, bukan "DELIVERED" 3x

Solusi (Idempotency):
Setiap ketukan punya ID unik (event_id).
Kamu catat: "ID-123 sudah diproses" → ketukan berikutnya dengan ID yang sama → skip.

Idempotency di payment = MANDATORY. Tanpanya → user dicharge 2x, saldo bertambah 2x, stok berkurang 2x.
    

Webhook tanpa idempotency seperti kurir yang ketuk pintu 3x dan kamu proses pesanan 3x. Idempotency = catat ID setiap event.

Penjelasan Konsep

Webhook adalah HTTP callback dari payment gateway ke backend-mu saat terjadi event (payment success, refund, subscription update). Tapi webhook tidak guaranteed exactly-once — PG bisa kirim duplicate events.

Kenapa idempotency wajib: (1) Network retry — kalau webhook gagal (timeout, error), PG akan retry. Satu transaksi bisa menghasilkan multiple webhook calls. (2) Race condition — user refresh halaman + webhook datang bersamaan. (3) Without idempotency: user saldo bertambah 2x, stok berkurang 2x, subscription aktif 2x.

Implementation: (1) Setiap webhook event punya event_id unik. (2) Sebelum proses, cek apakah event_id sudah pernah diproses (simpan di DB: processed_webhook_events). (3) Kalau sudah → return 200 OK (acknowledge receipt, jangan kirim error). (4) Kalau belum → proses dalam DB transaction + simpan event_id. (5) Gunakan idempotency key dari PG (Xendit: idempotency-key header, Stripe: idempotency_key di event).

Verifikasi signature: JANGAN percaya semua request ke webhook endpoint. Verifikasi signature/hash untuk memastikan request berasal dari PG, bukan attacker.

Inti yang Perlu Dipahami

Bagian ini berfokus pada memahami kenapa webhook payment BUTUH idempotency., mampu mengimplementasikan idempotent webhook handler., dan mampu menangani webhook retry dan duplicate events. Jangan terburu-buru menghafal istilahnya. Lebih penting untuk memahami peran setiap konsep dan kapan konsep itu muncul dalam pekerjaan web development.

Saat membaca Webhook & Idempotency, gunakan tujuan belajar sebagai penanda arah. Kalau kamu sudah bisa menjelaskan tujuan itu dengan kata-katamu sendiri, berarti fondasinya mulai terbentuk.

Cara Membayangkannya

Webhook tanpa idempotency seperti kurir yang ketuk pintu 3x dan kamu proses pesanan 3x. Idempotency = catat ID setiap event. Analogi ini dipakai supaya konsep teknis tidak terasa melayang. Hubungkan setiap istilah dengan perannya: siapa yang meminta, siapa yang memproses, data apa yang berpindah, dan hasil apa yang diharapkan.

Kalau analoginya sudah terasa masuk akal, barulah lihat istilah teknisnya. Cara ini membuat materi lebih mudah dipahami daripada langsung menghafal definisi.

Saat Melihat Contoh Kode

Contoh kode pada chapter ini memakai bahasa typescript. Bacalah contoh kode sebagai ilustrasi alur, bukan sebagai bagian yang harus langsung dihafal.

Perhatikan nama fungsi, urutan langkah, dan data yang berpindah. Biasanya tiga hal itu sudah cukup untuk memahami hubungan antara teori dan praktik.

Konteks dalam Perjalanan Belajar

Setiap konsep di platform ini dipilih karena dipakai di industri. Fokus pada pemahaman, bukan hafalan.

Kamu sudah di bagian lanjutan. Mulai pikirkan bagaimana konsep ini dipakai di dunia kerja — bukan hanya untuk belajar, tapi untuk membangun produk nyata.

Gunakan pertanyaan reflektif dan prompt AI di akhir chapter sebagai latihan aktif. Membaca saja tidak cukup — kamu perlu menjelaskan ulang dengan kata-katamu sendiri.

Contoh Kode

typescript
// Idempotent webhook handler
async function handlePaymentWebhook(req: Request) {
  const event = await req.json();
  const eventId = event.id || event.order_id; // PG-specific
  
  // Idempotency check
  const existing = await db.query.processedEvents.findFirst({
    where: eq(processedEvents.eventId, eventId),
  });
  
  if (existing) {
    return Response.json({ status: 'ok' }); // Already processed
  }
  
  // Process in transaction
  await db.transaction(async (tx) => {
    // Update order status
    await tx.update(orders).set({ status: 'paid' }).where(eq(orders.id, event.order_id));
    
    // Grant access (digital product)
    await tx.insert(userProducts).values({
      userId: event.user_id,
      productId: event.product_id,
    });
    
    // Mark event as processed
    await tx.insert(processedEvents).values({ eventId });
  });
  
  return Response.json({ status: 'ok' });
}

Prompt AI

Implementasi webhook handler untuk Midtrans/Xendit. Requirements: idempotency, signature verification, transaction-based processing, dan retry handling.

Pertanyaan Reflektif

Cek webhook handler-mu: apakah idempotent? Kalau PG kirim event yang sama 2x, apa yang terjadi? Coba kirim webhook yang sama 2x — apakah menghasilkan duplicate?