Webhook Nedir? Ödeme Sistemlerinde İmza Doğrulama, Retry ve Güvenlik
Webhook, bir olay olduğunda dış sistemin sizin verdiğiniz URL'ye HTTP POST attığı bildirimdir. Ödeme alındı, iade tamamlandı, 3D sonuçlandı: banka veya ödeme kuruluşu sizi aramaz; sizin endpoint'inize yazar. Sizin işiniz cevabı 2xx vermek, imzayı doğrulamak ve aynı olayı ikinci kez işlememekdir.
Polling ile karıştırmayın. Polling'de siz periyodik sorarsınız. Webhook'ta karşı taraf olay anında iter. Gecikme düşer; buna karşılık güvenlik sizin omzunuza biner. Herkes sizin URL'nizi bilebilir. İmza yoksa sahte "ödeme başarılı" bildirimi siparişi kargaya çıkarır. Ödeme geçidinde callback doğrulamasını ödeme yazısında tutar ve hash ile kurmuştum. Burada aynı disiplini genel webhook sözleşmesine yazıyorum.
Webhook ne işe yarar?
- Ödeme sonucu: success, fail, chargeback
- 3D Secure callback sonrası asenkron netice
- Kargo durum değişimi, fatura kesildi, abonelik yenilendi
- CI/CD: build bitti, deploy oldu
Senkron cevap yetmezse webhook vardır. Kullanıcı 3D sayfasındayken tarayıcı kapanabilir; banka yine sizin URL'nize yazar. Bu yüzden ödeme durumunun tek kaynağı ekran cevabı değil, doğrulanmış olaydır.
İmza doğrulama olmadan webhook yoktur
Gelen isteğin gövdesini olduğu gibi güvenmeyin. Sağlayıcı, paylaşılan sırla HMAC üretir; siz aynı sırrla yeniden hesaplar, sabit süreli karşılaştırırsınız. Eşleşmezse 401. Gövdeyi parse etmeden önce ham baytı doğrulayın. JSON'a çevirip tekrar serialize etmek baytı bozar; imza kırılır veya sahte imza geçer.
public static bool VerifyHmacSha256(
byte[] rawBody,
string headerSignature,
string secret)
{
// header örn: "sha256=abcdef..."
var given = headerSignature.StartsWith("sha256=", StringComparison.OrdinalIgnoreCase)
? headerSignature["sha256=".Length..]
: headerSignature;
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = hmac.ComputeHash(rawBody);
var expected = Convert.ToHexString(hash); // veya Base64; sağlayıcıya göre
return CryptographicOperations.FixedTimeEquals(
Encoding.ASCII.GetBytes(expected.ToLowerInvariant()),
Encoding.ASCII.GetBytes(given.ToLowerInvariant()));
}
[HttpPost("webhooks/payments")]
public async Task<IActionResult> PaymentWebhook(CancellationToken ct)
{
Request.EnableBuffering();
using var ms = new MemoryStream();
await Request.Body.CopyToAsync(ms, ct);
var raw = ms.ToArray();
var signature = Request.Headers["X-Signature"].ToString();
if (!VerifyHmacSha256(raw, signature, _opts.WebhookSecret))
return Unauthorized();
// timestamp skimming: eski imzalı gövdeyi tekrar oynatma
var ts = Request.Headers["X-Timestamp"].ToString();
if (!IsFresh(ts, TimeSpan.FromMinutes(5)))
return Unauthorized();
var evt = JsonSerializer.Deserialize<PaymentWebhookEvent>(raw)!;
await _handler.HandleOnceAsync(evt, ct); // olay kimliğine göre idempotent
return Ok();
}
Timestamp kontrolü replay saldırısını keser. Saldırgan eski, geçerli imzalı bir paketi saklayıp sonra tekrar atamasın. Sağlayıcı timestamp vermiyorsa olay kimliğini ve işlenmişler tablosunu daha sıkı tutarsınız.
Retry: 2xx yoksa olay tekrar gelir
Sağlayıcı 2xx görmezse bildirimi yeniden yollar. Tipik düzen üstel geri çekilmedir: 1 dk, 5 dk, 30 dk, 2 saat. Sizin endpoint 500 dönerse veya 30 saniyede cevap vermezse aynı olay beş kez gelebilir. Bu yüzden webhook handler'da uzun iş yapmayın.
- İmzayı doğrula
- Olay kimliğini kaydet (unique)
- Kuyruğa at veya outbox'a yaz
- Hemen 200 dön
Sipariş güncelleme, e-posta, ERP sync worker'da yürüsün. Dual-write riskini Transactional Outbox yazısında kapatmıştım. Webhook cevabı ile iş mantığını aynı transaction'da kilitlemek timeout üretir; timeout da yeniden webhook üretir.
Aynı olay iki kez gelirse
Ağ koptu, sizin 200'ünüz karşıya gitmedi, sağlayıcı yeniden attı. Veya sağlayıcı kendi içinde duplicate üretti. Çözüm, istemci idempotency key'inden ayrı bir olay kimliğidir: event_id, notification_id, banka orderId+status bileşimi.
public async Task HandleOnceAsync(PaymentWebhookEvent evt, CancellationToken ct)
{
var inserted = await _db.TryInsertEventAsync(evt.EventId, ct);
if (!inserted)
return; // daha önce işlendi; sessizce çık, yine 200
if (evt.Status == "SUCCESS")
await _orders.MarkPaidAsync(evt.OrderId, evt.Amount, ct);
}
İkinci gelişte yine 200 dönün. 409 veya 500 dönmek sonsuz retry doğurur. "Zaten işlendi" sizin sorununuzdur; sağlayıcıya yeni hata kodu olarak yansıtmayın. API tarafındaki idempotency key istemcinin niyetini korur; webhook olay kimliği sağlayıcının bildirimini korur. İkisi farklı kapıdır.
Güvenlik kontrol listesi
- HTTPS zorunlu. Düz HTTP webhook URL'si kabul etmeyin.
- HMAC veya sağlayıcının belgelediği imza. IP allowlist tek başına yetmez; IP değişir, CDN arkası bozar.
- Sırrı konfigürasyonda tutun; koda gömmeyin. Rotasyon için iki sırı bir süre birlikte doğrulayın.
- Ham gövdeyi loglamayın. Kart, IBAN, TCKN düşer. Korelasyon numarası ve event_id yeter. KVKK listesi.
- Webhook URL'sini yönetim panelinde görmeyen role vermeyin. URL + sır sızarsa sahte olay üretilir.
- Tutar ve para birimini kendi transaction kaydınızla karşılaştırın. İmza geçer, tutar değişmiş olabilir; MitM veya hatalı entegrasyon ikisini de üretir.
Test nasıl yapılır?
Canlı bankaya sahte success atmayın. Sağlayıcının test imzasını veya kendi HMAC aracınızı kullanın. Yerelde tunnel (ngrok vb.) ile URL açılır; sır test sırrı olmalıdır. Bir birim test ham gövde + bilinen sır + beklenen imza üçlüsünü sabitler. İmza fonksiyonunu "gözümle baktım doğru" diye bırakmayın; bir bayt kayması production'da tüm bildirimleri 401 yapar.
Kısa cevaplar
Webhook nedir?
Bir olay gerçekleşince dış sistemin sizin HTTP endpoint'inize POST ile bildirim göndermesidir. Siz periyodik sormazsınız; karşı taraf iter.
Webhook ile API callback aynı şey mi?
Pratikte çoğu ödeme callback'i webhook düzenindedir: sizin URL'niz, onların POST'u. Fark isimlendirmededir. Asıl olan imza, tutar doğrulama ve idempotent işlemedir.
Webhook güvenliği nasıl sağlanır?
Paylaşılan sırla HMAC imza doğrulanır, timestamp ile replay kesilir, olay kimliğiyle mükerrer işlem engellenir. Yalnız IP kısıtı yeterli değildir.
Neden hemen 200 dönülür?
Sağlayıcı 2xx görmezse aynı olayı yeniden yollar. Uzun ERP veya e-posta işi handler içinde yapılırsa timeout ve fırtına retry doğar. Doğrula, kaydet, kuyruğa at, 200 dön.
Aynı webhook iki kez gelirse ne olur?
Olay kimliği daha önce işlendiyse iş mantığı tekrar çalışmaz; yine 200 dönülür. 500 dönmek yeniden gönderimi körükler.
Kaynaklar
- OWASP: Webhook güvenlik pratikleri — imza, replay, sır yönetimi.
- Sağlayıcı dokümanı: Stripe / iyzico / banka callback imza alan adları birbirinden farklıdır; önce ham gövde kuralını okuyun.