Webhook'lar
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
- 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.
- 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.
- Bir arka plan dispatcher'ı kuyruğu okur, payload'u
eventsfiltresi eşleşen, etkin ve sessize alınmamış her aboneye POST eder ve her teslimatı kaydeder. - 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.
{
"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-logsiçindeki satırınid'siyle eşleşir,actorişlemi yapanı gösterir (admin,hmac,reseller,client, …),extraolaya 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/jsonUser-AgentWHost-Webhook/1.0X-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
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-Timestampdeğ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'tenode:cryptomodülündencrypto.timingSafeEqual— iki buffer'ın uzunluğu aynı olmalıdır).
PHP'de doğrulama (whost-php-sdk)
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
# 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ıidile 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/16100.64.0.0/10(CGNAT),0.0.0.0/8169.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.255dahil),::/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:
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ü):
{
"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
POST /api/v1/system/webhooks/{id}/rotate
Yeni düz metin gizli anahtarı JSON yanıt gövdesinde bir kez döndürür:
{
"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ışı:
- Yeni gizli anahtarı sakin bir döngüde üretin (yoğun olmayan saatlerde).
- Ö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.
- Rotasyon API çağrısını tetikleyin. Agent o andan itibaren her yeni teslimatı yeni gizli anahtarla imzalar.
- 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.
- İ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
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.
Yukarıda bulamadığınız her şey için destek ekibimiz her zaman yanınızda.