Webhook'lar

Güncellendi 3 Eki 2026 Markdown

Bir WHost agent'ından abone endpoint'lerine push tabanlı olay teslimatı. Partner faturalama sistemlerinin hesap / domain / SSL / yedekleme yaşam döngüsü olaylarını, agent üzerinde gerçekleştiği anda öğrenmesi için polling yerine webhook kullanın.


Nasıl çalışır

  1. Sistem yöneticisi, yönetici panelinde (veya API üzerinden — bkz. Kayıt) bir abone URL'si kaydeder; imzalama gizli anahtarını, yönetici kendisi vermezse agent üretir.
  2. Katalogdaki bir olaya karşılık gelen bir operasyon denetim loguna her yazıldığında, agent'ın denetim logu köprüsü olayı disk üzerindeki bir kuyruğa ekler.
  3. Bir arka plan dispatcher'ı kuyruğu okur, payload'u events filtresi eşleşen, etkin ve sessize alınmamış her aboneye POST eder ve her teslimatı kaydeder.
  4. Başarısız teslimatlar üstel geri çekilme (exponential backoff) ile tekrar denenir; 5. başarısız denemeden sonra teslimat dead-letter'lanır (manuel tekrar deneme için teslimat geçmişinde görünür).

Olay kataloğu

Kararlı, açık (public) dot-notation tanımlayıcılar. Yeni olaylar eklenir; mevcut isimler asla değişmez. Tam makine-okunabilir katalog: GET /api/v1/system/webhooks/events.

account.createdHesap sağlandı.
account.suspendedHesap askıya alındı (panel veya API).
account.unsuspendedHesabın askısı kaldırıldı.
account.terminatedKalıcı silme tamamlandı.
account.package_changedPlan / paket değişimi (change-package).
account.password_changedBir hesabın panel parolası değiştirildi veya sıfırlandı (yönetici, bayi, hesap sahibi ya da e-postayla gelen sıfırlama bağlantısıyla). Yönetici kendi admin parolasını değiştirdiğinde de gönderilir — bu durumda username null olur.
domain.addedAddon domain eklendi (subdomain ve park edilmiş domainler olay göndermez).
domain.removedAddon domain kaldırıldı.
ssl.issuedSertifikası olmayan bir domaine sertifika kuruldu: panel ya da API üzerinden bir Let's Encrypt isteği veya özel sertifika yüklemesi, ya da yeni bir addon domainin otomatik sertifikası. Yeni hesabın otomatik sertifikası ve toplu (bulk) sertifika üretimi için gönderilmez.
ssl.renewedZaten sertifikası olan bir domain için panel ya da API üzerinden Let's Encrypt sertifikası yeniden istendi ve yeni sertifika üretildi. certbot'un kendi zamanlamasıyla (systemd timer ya da cron) yaptığı yenilemeler olay göndermez.
ssl.failedPanel ya da API üzerinden yapılan bir Let's Encrypt isteği veya özel sertifika yüklemesi sunucu tarafında başarısız oldu; toplu üretimde başarısız olan her domain için. Otomatik sertifikalar (yeni hesap, yeni addon domain) ve certbot'un zamanlanmış yenilemeleri olay göndermez.
backup.completedBir yedek arşivi oluşturuldu (manuel veya zamanlanmış); başarısız bir uzak hedef yüklemesi extra.remote_upload_error alanında bildirilir.
backup.failedBir yedekleme girişiminden arşiv çıkmadı.
database.createdMariaDB veritabanı sağlandı.
database.deletedVeritabanı silindi.
email.createdPosta kutusu sağlandı.
email.deletedPosta kutusu kaldırıldı.
ftp.createdFTP hesabı sağlandı.
ftp.deletedFTP hesabı kaldırıldı.
dns.record_addedDNS kaydı eklendi.
dns.record_updatedDNS kaydı değiştirildi.
dns.record_deletedDNS kaydı kaldırıldı.
plan.createdHosting paketi eklendi.
plan.updatedHosting paketi değiştirildi.
plan.deletedHosting paketi kaldırıldı.

Payload yapısı

Her giden teslimat, kararlı bir zarf (Stripe tarzı) ile JSON formatındadır. Burada girintili gösterilir; gövde sıkıştırılmış (compact) JSON olarak gönderilir.

JSON
{
  "id":      "evt_4f2c8c1e9b3d40e98c2e1cf7e9c9c3a2",
  "event":   "account.created",
  "created": 1748640123,
  "data": {
    "audit_id":   "1c2a4f3e-...",
    "actor":      "admin",
    "username":   "demo01",
    "description":"Account 'demo01' created with plan 'starter'",
    "ip_address": "203.0.113.4",
    "extra": {
      "domain":   "demo01.example.com",
      "plan":     "starter"
    },
    "timestamp":  "2026-05-23T10:42:03.512947+00:00"
  }
}
  • id — olay başına benzersizdir. Abone tarafında idempotent yinelemeleri ayıklamak için bunu kullanın; agent zaman aşımı, taşıma (transport) hatası ya da 2xx olmayan her yanıttan sonra tekrar dener ve manuel tekrar deneme aynı id'yi yeniden gönderir, bu nedenle yinelenen POST'lar beklenir.
  • event — yukarıdaki katalog tanımlayıcılarından biri.
  • created — unix saniyesi; agent'ın kuyruğa alma anındaki saati.
  • data — olaya özgü payload. Bir operasyonun doğurduğu olayda denetim logu satırıdır: audit_id, GET /api/v1/audit-logs içindeki satırın id'siyle eşleşir, actor işlemi yapanı gösterir (admin, hmac, reseller, client, …), extra olaya göre değişir. Test olayı bunun yerine {"test": true, "endpoint_id": …, "note": …} taşır (bkz. Test fire).

Giden HTTP header'ları

Content-Typeapplication/json
User-AgentWHost-Webhook/1.0
X-WHost-Webhook-EventOlay adı (account.created, ...) — gövdedeki event ile aynı; alıcının önündeki bir yönlendirici gövdeyi ayrıştırmadan yönlendirme yapabilir.
X-WHost-Webhook-Event-Idevt_<32 hex> — gövdedeki id ile aynı.
X-WHost-Webhook-Delivery-Iddlv_<32 hex> — teslimat başına bir tane: bir teslimatın otomatik tekrar denemeleri aynısını kullanır, manuel tekrar deneme yenisini alır; agent'ın teslimat geçmişinde görünür.
X-WHost-Webhook-Timestampİmza üretildiğindeki unix saniyesi (her denemede yeniden üretilir).
X-WHost-Webhook-Signaturesha256=<hex> — "{timestamp}.{raw_body}" ifadesinin uç nokta gizli anahtarıyla anahtarlanmış HMAC-SHA256'sı, küçük harfli hex.

İmzalama modeli

metin
signature = HMAC_SHA256(
    secret,
    timestamp + "." + raw_body
)
  • Her zaman ham (raw) gövde baytlarına karşı doğrulama yapın. Abone tarafında JSON'u yeniden kodlamak anahtarları / boşlukları yeniden sıralar ve imzayı bozar.
  • X-WHost-Webhook-Timestamp değeri saatinizden ±300 saniyeden fazla saparsa teslimatları reddedin — bu replay'i engeller.
  • Hesaplanan HMAC'i karşılaştırırken sabit zamanlı karşılaştırma kullanın (PHP'de hash_equals, Node.js'te node:crypto modülünden crypto.timingSafeEqual — iki buffer'ın uzunluğu aynı olmalıdır).

PHP'de doğrulama (whost-php-sdk)

PHP
use WHost\Http\WebhookVerifier;

$body = file_get_contents('php://input');
$sig  = $_SERVER['HTTP_X_WHOST_WEBHOOK_SIGNATURE']  ?? '';
$ts   = $_SERVER['HTTP_X_WHOST_WEBHOOK_TIMESTAMP'] ?? '';

try {
    $event = WebhookVerifier::parse($body, $sig, $ts, $WEBHOOK_SECRET);
} catch (\WHost\Exceptions\WHostException $e) {
    http_response_code(400);
    exit;
}

// $event is the decoded envelope (id, event, created, data).

SDK olmadan doğrulama

kabuk
# Bash — verify the HMAC manually
expected=$(printf '%s.%s' "$ts" "$body" | openssl dgst -sha256 -hmac "$secret" | awk '{print $2}')
test "sha256=$expected" = "$received_signature"

Tekrar deneme + dead-letter

Başarısız teslimatlar (zaman aşımı ya da başka bir transport hatası veya 2xx olmayan her yanıt — yönlendirmeler izlenmez) şu programa göre tekrar denenir:

Deneme Tekrar denemeden önceki gecikme
1 anında
2 1s
3 10s
4 60s
5 5 dk
5+ dead-letter

Her deneme kaydedilir: dispatcher hâlâ yeniden denerken teslimat attempts ve next_attempt_at ile failed okunur; aynı delivery_id için yazılan son kayıt (başarı ya da dead_letter) onun yerini alır. Agent, bir teslimat sonraki denemesini beklerken durursa teslimat son kayıt olarak failed kalır ve otomatik olarak yeniden denenmez — manuel olarak yeniden deneyin.

dead_letter durumundaki bir teslimat GET /api/v1/system/webhooks/deliveries?status=dead_letter adresinde görünür ve POST /api/v1/system/webhooks/deliveries/{id}/retry üzerinden manuel olarak yeniden kuyruğa alınabilir.

Aboneler ŞUNLARI YAPMALI:

  • Kabul ettikleri herhangi bir payload için 2xx döndürmeli (bilinmeyen olay tipleri dahi) — agent 2xx olmayanı bir teslimat başarısızlığı olarak kabul eder.
  • Hızlıca onaylamalı (< 5s); agent bireysel denemeleri 10 saniyede zaman aşımına uğratır.
  • Ağır işlemeyi istek yolundan çıkarmalı (kuyruk, worker) — böylece ACK zaman aşımından önce dönmüş olur.
  • Gövdedeki id üzerinden tekrarları ayıklamalı — bir teslimat aynı id ile en fazla 5 deneme yapar ve manuel tekrar deneme onu yeniden gönderir; tek bir olay için sayaçları asla iki kez artırmayın.

SSRF savunmaları

Agent yalnız http:// ve https:// URL'lerini kabul eder; özel, loopback, link-local, CGNAT veya belirsiz (unspecified) adreslere çözümlenen URL'lere kayıt yapmayı veya teslimat yapmayı reddeder:

  • 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
  • 100.64.0.0/10 (CGNAT), 0.0.0.0/8
  • 169.254.0.0/16 (link-local)
  • 127.0.0.0/8, ::/128, ::1/128, fc00::/7, fe80::/10
  • IPv4-mapped IPv6 adresleri (::ffff:…) taşıdıkları IPv4 adresi olarak kontrol edilir
  • hiçbir zaman genel bir hedefi adlandırmayan aralıklar da: 192.0.0.0/24, 198.18.0.0/15, 224.0.0.0/4 (multicast), 240.0.0.0/4 (ayrılmış, 255.255.255.255 dahil), ::/96, fec0::/10, ff00::/8, 64:ff9b:1::/48; NAT64 (64:ff9b::/96) ya da 6to4 (2002::/16) adresi taşıdığı IPv4 adresi olarak kontrol edilir

Çözümlenemeyen bir hostname de reddedilir.

DNS çözümlemesi teslimat anında yeniden kontrol edilir — sonradan engelli bir adrese çevrilen ya da artık çözümlenemeyen kayıtlı bir hostname, istek gönderilmeden dead-letter'lanır (0 deneme; kaydın error alanı ssrf_block ile başlar).

Bir teslimatın her denemesi bu kontrolün onayladığı adreslere bağlanır: hostname kontrol ile bağlantı arasında yeniden çözümlenmez, yeniden denemeler de aynı adreslere gider. İstek hostname'i Host başlığında ve HTTPS'te TLS sunucu adı olarak taşır; sertifika doğrulaması değişmez. Birden çok adresi olan bir hostname'de adresler çözümleyici sırasıyla denenir; bir sonraki adrese yalnız öncekine bağlantı açılamadığında geçilir.


Kayıt

Yönetici paneli

Ayarlar (üst çubuktaki dişli simgesi) → Tüm Ayarlar → Webhook'lar → Yeni uç nokta. Gizli anahtar alanını boş bırakırsanız anahtar üretilir; isterseniz kendi anahtarınızı girin (16–128 yazdırılabilir ASCII karakter, boşluksuz). Uç nokta kaydedildikten sonra panel gizli anahtarı Webhook gizli anahtarı oluşturuldu diyaloğunda bir kez gösterir — o anda aboneye kopyalayın.

API (HMAC)

İmza tam yolu (/api/v1/system/webhooks) ve gönderilen gövde string'inin tam hâlini kapsar:

kabuk
BODY='{"url":"https://billing.example.com/whost/webhook","events":["account.created","account.suspended","account.terminated"],"description":"Billing system order lifecycle"}'
TS=$(date +%s)
NONCE=$(openssl rand -hex 16)
SIG=$(printf '%s\n%s\n%s\n%s\n%s' POST /api/v1/system/webhooks "$TS" "$NONCE" "$BODY" \
  | openssl dgst -sha256 -hmac "$WHOST_API_SECRET" | awk '{print $NF}')

curl -X POST https://hosting.example.com/api/v1/system/webhooks \
  -H "X-WHost-Key: $WHOST_API_KEY" \
  -H "X-WHost-Timestamp: $TS" \
  -H "X-WHost-Nonce: $NONCE" \
  -H "X-WHost-Signature: $SIG" \
  -H "Content-Type: application/json" \
  --data-binary "$BODY"

events en az bir olay adlandırmalıdır; boş liste oluşturmada da güncellemede de 400 WEBHOOK_VALIDATION ile reddedilir. secret isteğe bağlıdır: gönderilmezse 43 karakterlik rastgele bir gizli anahtar üretilir; gönderilirse 16–128 yazdırılabilir ASCII karakter olmalı ve boşluk içermemelidir.

Yanıt (düz metin gizli anahtarın tek seferlik görünümü):

JSON
{
  "status": "success",
  "data": {
    "id": "whk_a1b2c3...",
    "url": "https://billing.example.com/whost/webhook",
    "secret": "<üretilen gizli anahtar, 43 karakter>",
    "events": ["account.created", "account.suspended", "account.terminated"],
    "description": "Billing system order lifecycle",
    "enabled": true,
    "created_at": "2026-05-23T10:00:00.418204+00:00",
    "updated_at": "2026-05-23T10:00:00.418311+00:00",
    "muted_until": null
  },
  "message": "Webhook endpoint registered. Save the secret — it won't be shown again."
}

Gizli anahtar rotasyonu

metin
POST /api/v1/system/webhooks/{id}/rotate

Yeni düz metin gizli anahtarı JSON yanıt gövdesinde bir kez döndürür:

JSON
{
  "status": "success",
  "data": { "endpoint_id": "whk_…", "secret": "<yeni gizli anahtar, 43 karakter>" },
  "message": "Secret rotated. Subscriber must use this value immediately."
}

Hemen kopyalayın — GET /api/v1/system/webhooks/{id} (liste ve güncelleme yanıtı gibi) gizli anahtarı maskeli döndürür: son dört karakter dışındaki her karakter * ile değiştirilir. Gizli anahtarı kaybederseniz, yeniden rotasyon yapın.

Neden rotasyon?

  • Abone tarafında sızıntı şüphesi (CI logları, hata izleyici, yedek dump'ı).
  • Operatör değişimi — gizli anahtarı ilk kaydeden kişi ayrılıyor.
  • Zamanlanmış rotasyon politikası (her 90 günde bir yaygın bir SOC gereksinimidir).

Yuvarlanan (rolling) pencere yok — geçişi koordine edin.

Rotasyon çağrısı 200 döndükten sonra gönderilen her teslimat yeni gizli anahtarla imzalanır. O anda tekrar deneme programında olan teslimatlar ise son denemelerine kadar (ilk denemelerinden sonra en fazla yaklaşık yedi dakika) başladıkları gizli anahtarla imzalamaya devam eder. Aboneler yeni değeri sonraki olay ateşlenmeden önce kabul etmek zorundadır, aksi takdirde imza doğrulaması başarısız olur ve teslimat tekrar deneme kuyruğuna düşer (sonunda dead-letter).

Önerilen abone tarafı rotasyon akışı:

  1. Yeni gizli anahtarı sakin bir döngüde üretin (yoğun olmayan saatlerde).
  2. Önce, abone güncellemesini hem eski HEM de yeni gizli anahtarı kabul edecek şekilde yayına alın (önce eskiyi deneyin, yeniye geri düşün). Bu çağrı başına bir try/except'tir, config bayrağı değil — doğrulamayı ucuz tutar ve yarış pencerelerini önler.
  3. Rotasyon API çağrısını tetikleyin. Agent o andan itibaren her yeni teslimatı yeni gizli anahtarla imzalar.
  4. Test-fire endpoint'i üzerinden (bkz. Test fire), sizin tarafınızda gerçek bir imzalı payload'un artık yeni gizli anahtarla eşleştiğini onaylayın.
  5. İkinci deploy: eski gizli anahtar fallback yolunu kaldırın.

Bu üç adımlı koordinasyon, Stripe ve Slack'in webhook rotasyonları için önerdikleri aynı pattern'dir. İki gizli anahtarlı aboneler ayrıca "operatör rotasyona basıyor" ile "abone yeniden yayına alındı" arasındaki aralığı ve rotasyondan önce başlamış teslimatların tekrar denemelerini de karşılar.

İki-deploy pattern'ini çalıştıramazsanız (abone donmuş bir SaaS, vb.), önce muted_until üzerinden uç noktayı sessize alın, rotasyon yapın, abonenin gizli anahtarını güncelleyin, sonra sesi açın. Sessize alma / sesi açma tek bir PUT /api/v1/system/webhooks/{id} çağrısıdır: muted_until alanına ISO-8601 bir zaman verin (saat dilimi olmayan değer UTC okunur; sessizlik o zamanda kendiliğinden biter) ya da erken açmak için "muted_until": "" gönderin — null alanı değiştirmez. Sessize alma olayları bekletmez: uç nokta sessizdeyken (veya devre dışıyken) oluşan olaylar o uç için kuyruğa alınmaz ve sonradan da teslim edilmez — bunları GET /api/v1/audit-logs üzerinden mutabık kılın.


Teslimat geçmişi + manuel tekrar deneme

GET /api/v1/system/webhooks/deliveriesSayfalanmış liste, en yeni önce (limit 1–500, varsayılan 50; offset; filtre: endpoint_id, status — success, failed, dead_letter — ve event_type).
GET /api/v1/system/webhooks/deliveries/{id}Orijinal payload ve son yanıt alıntısı (ilk 512 karakter) dahil tam kayıt. Bilinmeyen id için 404 DELIVERY_NOT_FOUND.
POST /api/v1/system/webhooks/deliveries/{id}/retryOrijinal olayı (aynı olay id'siyle) teslimatın kendi ucuna yeniden kuyruğa alır; yeni teslimat yeni bir delivery_id alır. Uç silinmişse 404 WEBHOOK_NOT_FOUND, devre dışı ya da sessize alınmışsa 409 WEBHOOK_DISABLED, saklanan payload yoksa 400 DELIVERY_PAYLOAD_MISSING.

Test fire

metin
POST /api/v1/system/webhooks/{id}/test

Yalnız bu uca adreslenmiş sentetik bir olayı kuyruğa alır (endpoint'in filtre listesindeki ilk olay); aynı olaya abone diğer uçlar bunu almaz. data alanı {"test": true, "endpoint_id": "whk_…", "note": "Synthetic webhook fired from admin panel"} şeklindedir — yan etki üretmeden işleyin. Devre dışı ya da sessize alınmış uç 409 WEBHOOK_DISABLED döner. Endpoint'i üretim trafiğine güvenmeden önce ağ bağlantısını + imza doğrulamasını teyit etmek için yararlıdır.

Hâlâ Yardıma mı İhtiyacınız Var?

Yukarıda bulamadığınız her şey için destek ekibimiz her zaman yanınızda.