Tüm yazılar

Webhook en iyi pratikleri: Standard Webhooks spec, signature verify, retry pattern

Sendnomi webhook'larıyla idempotent, güvenli, retry-tolerant entegrasyonlar. HMAC imza doğrulama, dead-letter handling, exponential backoff.

Webhook bir push-mekanizmadır: Sendnomi senin endpoint’ine HTTP POST ile event gönderir, sen 2xx döndürürsün. Senkron, basit, ölçeklenebilir. Ama production’da çalıştığında dört kritik problem ortaya çıkar:

  1. Güvenlik — kimden geldiğini nasıl ispatlarsın?
  2. Idempotency — aynı event iki kez geldiğinde ne olur?
  3. Retry davranışı — sen düştüğünde Sendnomi ne yapar?
  4. Dead-letter — sürekli başarısız event’i nasıl ele alırsın?

Bu yazıda her birinin Sendnomi tarafındaki çözümünü ve senin tarafında kodlayacağın pattern’i göreceğiz.

1. Standard Webhooks Spec

Sendnomi webhook’ları standardwebhooks.com açık spec’ini takip eder. Üç temel header:

webhook-id: msg_2KqWfX...        # event'in eşsiz kimliği (idempotency anahtarı)
webhook-timestamp: 1716895200    # event'in oluşturulma anı (replay attack koruması)
webhook-signature: v1,K6xYBy... # HMAC-SHA256 imzası

2. İmza doğrulama (HMAC-SHA256)

Senin endpoint’in body’yi okur ve şu adımları izler:

import crypto from 'crypto';

function verifyWebhook(
  payload: string,        // raw request body — JSON.parse ÖNCESİ
  headers: Record<string, string>,
  secret: string,         // panel → webhook → signing secret
): boolean {
  const id = headers['webhook-id'];
  const ts = headers['webhook-timestamp'];
  const sig = headers['webhook-signature'];

  if (!id || !ts || !sig) return false;

  // 1. Timestamp tolerance check — 5 dakikadan eski reject
  const age = Math.abs(Date.now() / 1000 - parseInt(ts, 10));
  if (age > 300) return false;

  // 2. HMAC hesapla
  const signedPayload = `${id}.${ts}.${payload}`;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('base64');

  // 3. Timing-safe compare — burada `===` KULLANMA (timing attack)
  const provided = sig.split(',')[1]; // "v1,<base64>" formatı
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(provided),
  );
}

Kritik noktalar:

  • payload ham gövde olmalı — JSON.parse’tan SONRA stringify olmaz, byte byte aynı olması gerekir.
  • Timing-safe compare olmazsa attacker brute-force ile imzayı tahmin edebilir.
  • Timestamp tolerance kabaca 5 dakika — Sendnomi tarafında saat senkron, sende NTP açık olmalı.

3. Idempotency — aynı event iki kez geldiğinde

Webhook retry mekanizması olduğu için bir event 1+ kez gelebilir. Senin handler’ın bu event’i daha önce işledim mi? sorusuna kesin cevap vermeli.

Pattern: webhook-id’yi DB’ye kayıtla

async function handleWebhook(event: WebhookEvent, headers: Headers) {
  const eventId = headers['webhook-id'];

  // 1. Daha önce işlendi mi?
  const existing = await db.processedWebhook.findUnique({
    where: { eventId },
  });
  if (existing) {
    return { status: 200, body: 'already-processed' };
  }

  // 2. Atomik insert + process — race condition'a karşı UNIQUE constraint
  try {
    await db.processedWebhook.create({
      data: { eventId, processedAt: new Date() },
    });
  } catch (e) {
    if (e.code === 'P2002') { // unique violation
      return { status: 200, body: 'race-already-processed' };
    }
    throw e;
  }

  // 3. Asıl iş
  await processBusinessLogic(event);
  return { status: 200, body: 'ok' };
}

90 günden eski processedWebhook kayıtlarını cron ile temizleyebilirsin — Sendnomi 7 gün sonra retry vermez.

4. Retry davranışı

Sendnomi tarafında varsayılan retry policy:

DenemeBeklemeToplam yaş
1 (ilk push)0
21 dakika1 dk
35 dakika6 dk
430 dakika36 dk
52 saat2h 36dk
66 saat8h 36dk
712 saat20h 36dk
8 (son)24 saat44h 36dk

Senin endpoint’in 2xx döndürmediği sürece deniyoruz. 8. denemeden sonra event dead-letter’a düşer.

Hangi response retry tetikler?

  • 5xx hata — retry
  • Connection timeout, DNS hatası — retry
  • 2xx — başarı, retry yok
  • 4xx — kalıcı hata, retry YOK (4xx’in geri çağırılması imkansız olduğu için)

4xx ile retry istemiyorsan:

  • 400 — payload format hatası (sen düzeltmedikçe tekrar gelecek)
  • 401/403 — imza yanlış (Sendnomi panelinden secret’ı yenile)
  • 410 — endpoint kalıcı kapandı

5. Dead-letter pattern

  1. denemede de başarısız olursa Sendnomi event’i dead-letter queue’sunda 30 gün saklar. Panel → Webhook → Dead-letter sekmesinden:
  • Event JSON’unu görebilirsin
  • Manuel olarak retry tetikleyebilirsin (endpoint düzeldikten sonra)
  • Event’i fail olarak kapatabilirsin (artık ilgili değil)

Senin tarafta da kendi dead-letter’ını kurman önerilir:

async function handleWebhook(event, headers) {
  try {
    // ... idempotency check + business logic
  } catch (err) {
    // Hatayı logla ama 5xx döndürmeden önce risk değerlendir
    await dlq.push({ event, err: err.message, headers });
    if (isTransient(err)) {
      return { status: 500, body: 'retry-me' };  // Sendnomi retry'a alır
    }
    return { status: 200, body: 'logged-to-dlq' }; // kendi DLQ'na yatır, ack ver
  }
}

6. Performans pattern’ları

Async processing

Webhook handler’ın 5 saniye altında 2xx dönmelidir. Ağır iş varsa:

async function handleWebhook(event, headers) {
  await verifySignature(...);
  await checkIdempotency(eventId);
  await jobQueue.enqueue('process-webhook', event); // BullMQ, sidekiq, vs.
  return { status: 200, body: 'queued' };
}

Connection pooling

Webhook endpoint yüksek frekanslı bir HTTP endpoint’tir. Reverse proxy’nde (nginx, Caddy) keepalive aktif olmalı; backend’in connection pool’u sağlam (HTTPie ile sürekli test edebilirsin).

Eventual consistency

Webhook senin DB’ne yazıyorsa ve dış sistem (CRM, analytics) aynı veriyi okuyorsa, eventual consistency window’unu (5-15 sn) UI’ında belirt — “yeni event’ler 10 saniye içinde görünür.”

Kontrol listesi

  • HMAC-SHA256 imza doğrulama + timing-safe compare
  • webhook-timestamp 5 dakika tolerance check
  • webhook-id ile idempotency (DB UNIQUE constraint)
  • 2xx response 5 saniye altında — ağır iş async kuyruğa
  • 4xx (validation) vs 5xx (transient) ayrımı net
  • Kendi DLQ’n var (Sendnomi’ninkine ek)
  • Health endpoint (Sendnomi panelinden ping atabilelim)

Sonuç

Webhook bir “push API”dir ve production’da güvenilir olması için her iki tarafın disiplinli olması gerekir. Sendnomi tarafı Standard Webhooks spec’ini takip eder; senin tarafta yukarıdaki pattern’larla idempotent, güvenli, retry-tolerant entegrasyon kurabilirsin.

Detay belgeler: /gelen-webhooklar/

— Yazılım Koçu