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:
- Güvenlik — kimden geldiğini nasıl ispatlarsın?
- Idempotency — aynı event iki kez geldiğinde ne olur?
- Retry davranışı — sen düştüğünde Sendnomi ne yapar?
- 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:
payloadham 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:
| Deneme | Bekleme | Toplam yaş |
|---|---|---|
| 1 (ilk push) | — | 0 |
| 2 | 1 dakika | 1 dk |
| 3 | 5 dakika | 6 dk |
| 4 | 30 dakika | 36 dk |
| 5 | 2 saat | 2h 36dk |
| 6 | 6 saat | 8h 36dk |
| 7 | 12 saat | 20h 36dk |
| 8 (son) | 24 saat | 44h 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
- 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