in

Webhook Tasarımı: İmza, Retry, Sıralama ve Idempotency

Webhook, API'nin tersidir: siz beklemezsiniz, karşı taraf sizi çağırır — ve ağ, zaman aşımı, tekrar teslim ve sıra bozulması sizin sorununuz olur. Sağlayıcı tarafında olay zarfı, HMAC imza, retry takvimi ve outbox; tüketici tarafında doğrulama, hızlı 200 ve asenkron idempotent işleme için üretimde işe yarayan tasarımı anlatıyoruz.

Sağlayıcıdan tüketiciye webhook teslimatını, imza doğrulamayı ve retry döngüsünü gösteren şema

Webhook, API'nin tersidir: veriyi siz sormazsınız, olay olduğunda karşı taraf sizin uç noktanızı çağırır. Ödeme sağlayıcısı "ödeme tamamlandı", kargo firması "teslim edildi", e-fatura entegratörü "fatura reddedildi" der. Basit görünür; sahada ise ağ hataları, zaman aşımları, aynı olayın üç kez gelmesi, olayların ters sırada ulaşması ve sahte istekler işi hızla karmaşıklaştırır. Kötü tasarlanmış bir webhook entegrasyonu ya olay kaçırır ya olayı iki kez işler.

Bu yazıda iki tarafı da ele alıyoruz: kendi ürününüz webhook gönderiyorsa (sağlayıcı) olay zarfı, HMAC imza, outbox'tan teslimat, retry takvimi ve sıralama; webhook alıyorsanız (tüketici) imza doğrulama, hızlı 200, asenkron ve idempotent işleme. Sonunda güvenlik kontrol listesi ve sık yapılan hatalar var.

Webhook mu, polling mi?

Ölçüt

Webhook

Polling (periyodik sorgu)

Gecikme

Saniyeler

Sorgu aralığı kadar (dakikalar)

Yük

Yalnızca olay olduğunda

Olay olmasa da her aralıkta istek

Güvenilirlik

Teslimat garantisi sağlayıcının retry'ına bağlı

Basit; kaçırılan olay bir sonraki sorguda yakalanır

Karmaşıklık

Dışa açık uç nokta, imza, idempotency

Yalnızca istemci mantığı, "son kontrol" imleci

Üretimde en sağlam kurulum ikisinin birleşimidir: webhook ana yol, düşük frekanslı bir mutabakat sorgusu (örneğin saatte bir "son bir saatin olayları") yedek yol. Kaçan webhook mutabakatta yakalanır.

Sağlayıcı tarafı: webhook göndermek

Olay zarfı

{
  "id": "evt_01J9X3ZK7Q8W2M",             // benzersiz olay kimliği → tüketici idempotency'si
  "type": "siparis.sevk_edildi",           // kaynak.olay biçimi, sürümlü katalog
  "version": 2,                            // şema sürümü
  "occurred_at": "2026-11-17T09:41:05Z",   // olay zamanı (teslimat zamanı değil)
  "sequence": 8123,                        // kaynak başına artan sıra numarası
  "data": { "siparis_no": "S-2026-004512", "kargo_takip": "TR123456789" }
}

İki tasarım kararı: ince mi, dolu mu? Dolu zarf (tüm veri içeride) tüketicinin ek çağrısını önler; ince zarf (yalnızca kimlik, tüketici veriyi API'den çeker) hassas veriyi webhook üzerinden taşımaz ve her zaman güncel durumu verir. Ödeme ve kişisel veri içeren olaylarda ince zarf tercih edilir.

Outbox'tan teslimat

Webhook, iş transaction'ının içinden HTTP çağrısıyla gönderilmez: tüketici yavaşsa sizin transaction'ınız bekler, tüketici çökmüşse olay kaybolur. Olay, iş verisiyle aynı transaction'da bir outbox tablosuna yazılır; ayrı bir teslimat işçisi abonelere gönderir, sonucu kaydeder ve başarısızlıkta yeniden dener.

HMAC imza

public String imzala(String zamanDamgasi, byte[] govde, byte[] gizliAnahtar) throws GeneralSecurityException {
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(gizliAnahtar, "HmacSHA256"));
    mac.update(zamanDamgasi.getBytes(StandardCharsets.UTF_8));
    mac.update((byte) '.');
    mac.update(govde);
    return HexFormat.of().formatHex(mac.doFinal());
}
// Başlıklar:
// Webhook-Id: evt_01J9X3ZK7Q8W2M
// Webhook-Timestamp: 1794000065
// Webhook-Signature: v1=3f2a…   (birden fazla imza olabilir: anahtar döndürme sırasında v1=eski,v1=yeni)

İmza gövdenin ham baytları üzerinden alınır; zaman damgası imzaya dahil edilir ki yakalanan bir istek günler sonra tekrar oynatılamasın. Gizli anahtar abone başına ayrıdır ve panelden döndürülebilir; döndürme sırasında iki anahtarla imzalanır.

Retry takvimi ve başarısızlık yönetimi

  • Başarı yalnızca 2xx'dir; 3xx takip edilmez, 4xx (401/403/404/410 gibi) yapılandırma hatasıdır ve retry yerine uyarı üretir, 5xx ve zaman aşımı retry'a girer.

  • Zaman aşımı kısa tutulur (5–10 sn); tüketici işi asenkron yapmalıdır.

  • Üstel takvim: 1 dk, 5 dk, 30 dk, 2 sa, 12 sa, 24 sa, 48 sa — toplam ~3 gün; her denemede jitter.

  • Takvim tükenince olay "teslim edilemedi" listesine düşer; abone panelden görür ve elle yeniden gönderebilir. Bir uç nokta günlerce başarısızsa abonelik otomatik askıya alınır ve e-posta ile bildirilir.

  • Abone başına eşzamanlılık sınırı ve devre kesici: yavaş bir abone diğerlerinin teslimatını geciktirmez.

Sıralama: garanti etmeyin, mümkün kılın

Retry'lar ve paralel teslimat nedeniyle olaylar sırayla ulaşmayabilir; "sevk edildi" olayı "hazırlanıyor"dan önce gelebilir. Sıra garantisi vermek teslimatı ciddi yavaşlatır. Bunun yerine zarfa sequence ve occurred_at koyun; tüketici kaynağın son bilinen sırasından küçük olayı atlar ya da gerektiğinde güncel durumu API'den çeker. Kesin sıra gereken senaryolar webhook değil, kuyruk işidir.

Tüketici tarafı: webhook almak

@PostMapping("/webhooks/odeme")
public ResponseEntity<Void> odemeWebhook(@RequestHeader("Webhook-Id") String olayId,
                                          @RequestHeader("Webhook-Timestamp") long zaman,
                                          @RequestHeader("Webhook-Signature") String imza,
                                          @RequestBody byte[] hamGovde) {
    if (Math.abs(Instant.now().getEpochSecond() - zaman) > 300) return ResponseEntity.status(400).build();   // 5 dk tolerans
    if (!dogrulayici.gecerli(zaman, hamGovde, imza)) return ResponseEntity.status(401).build();          // sabit zamanlı karşılaştırma
    boolean yeni = gelenOlaylar.kaydetYoksa(olayId, hamGovde);                                            // unique kısıt: tekrar gelen olay atlanır
    if (yeni) kuyruk.gonder(new WebhookIsle(olayId));                                                     // işleme asenkron
    return ResponseEntity.ok().build();                                                                   // hızlı 200: sağlayıcı beklemez
}

Dört kural: ham gövde üzerinden doğrula (JSON'a çevrilmiş metin imzayı bozar); önce kaydet, hemen 200 dön, sonra işle; olay kimliğiyle idempotent ol (aynı olay ikinci kez gelirse zaten kayıtlı, atlanır); işleme hatası webhook yanıtını etkilemez — hata sizin kuyruğunuzda yeniden denenir. İşleme sırasında olayın sırasını kontrol edin: kayıttaki son sequence'tan küçükse eski olaydır.

Güvenlik kontrol listesi

  • HTTPS zorunlu; imza doğrulaması olmayan uç nokta yok.

  • Zaman damgası toleransı ve olay kimliği tablosu ile tekrar oynatma koruması.

  • Gizli anahtar abone başına, döndürülebilir; kodda değil gizli yönetiminde.

  • Uç nokta URL'i tahmin edilemez ve kimliğe bağlı; isteğe bağlı IP izin listesi (sağlayıcı yayımlıyorsa).

  • Gövde boyutu sınırı ve hız sınırı; webhook ucu ana API ile aynı havuzu tüketmesin.

  • Ham gövde ve başlıklar loglanır (kişisel veri maskeli); sorun anında "ne geldi" sorusu cevaplanabilir.

Test ve operasyon

Sağlayıcı olarak abonelere "test olayı gönder" düğmesi ve teslimat geçmişi (istek, yanıt kodu, süre) sunun; entegrasyon sorunlarının çoğu bu ekranla çözülür. Tüketici olarak sağlayıcının sandbox'ını ve yerel geliştirmede bir tünel aracını kullanın; kaydedilen gerçek olayları test verisi olarak saklayıp entegrasyon testinde yeniden oynatın.

Sık yapılan hatalar

  1. Webhook'u iş transaction'ından göndermek — tüketici çökünce olay kaybolur, transaction bekler.

  2. Tüketicide senkron işlemek — 30 saniyelik iş sağlayıcının zaman aşımına takılır, retry gelir, iş iki kez yapılır.

  3. Olay kimliğine göre idempotency olmaması — retry'lar çift kayıt üretir.

  4. İmzayı JSON'a çevrilmiş gövdeden hesaplamak — alan sırası değişince imza tutmaz.

  5. Sıra garantisi varsaymak — "iptal" olayından sonra gelen "hazırlanıyor" siparişi yeniden açar.

  6. Mutabakat olmaması — üç günlük retry penceresi dolduğunda olay sessizce kaybolur.

Sık sorulan sorular

Webhook yerine Kafka ya da kuyruk kullanmalı mıyız?

Aynı kuruluş içindeki sistemler arasında kuyruk daha güvenilirdir. Webhook, dış müşteri ve iş ortaklarına — sizin kuyruğunuza erişimi olmayan taraflara — olay iletmenin standart yoludur.

Tüketici cevap vermezse olayları ne kadar saklamalıyız?

Retry penceresi kadar (3 gün) aktif; sonrasında teslim edilemeyenler 30 gün elle yeniden gönderilebilir durumda. Olay kataloğu API'siyle tüketici geçmiş olayları da sorgulayabilmelidir.

Bir olayı birden çok aboneye nasıl göndeririz?

Outbox'taki olay, abonelik tablosuna göre abone başına ayrı teslimat kaydına çoğaltılır; her teslimatın kendi durumu, denemesi ve imzası vardır.

Sonuç

İyi bir webhook tasarımı iki tarafın da işidir: sağlayıcı olay zarfı, outbox, HMAC imza ve retry takvimi ile "teslim etmeyi", tüketici imza doğrulama, hızlı 200 ve olay kimliğine dayalı idempotent asenkron işleme ile "işlemeyi" garanti eder. Sıra garanti edilmez ama zarftaki sıra numarasıyla yönetilir; kaçan olaylar mutabakatla yakalanır.

Ürününüze webhook altyapısı eklemek ya da mevcut webhook entegrasyonlarınızı güvenilir hale getirmek için entegrasyon ve otomasyon hizmetimize göz atabilirsiniz.