Lifecycle Webhook'ları ✓ Canlı

Kurulum, abonelik, teslim sağlığı ve GDPR (customer.redact) yaşam döngüsü event'leri, packet.created ile AYNI webhook ucuna düşer. Bunlara abone OLMAZSIN — bağlı (connected) kuruluma otomatik gönderilir (customer.redact yalnız PII scope'lu + consent vermiş kurulumlara); ödeme şartı yoktur. İmzalıdır (aynı HMAC) ve başarısız teslimlerde retry edilir.

Event'ten farkı: gating yok, her zaman gelir

Subscribable event (packet.created…)Lifecycle (app.installed…)
Abonelikevents[] + events:subscribe gerekirYok — her zaman gönderilir
Ödeme şartıYok — connected yeter
webhookUrlAynı webhookUrl
İmzainstall webhookSecret (HMAC)Aynı
Retryvarvar
Kimebağlı kurulumYalnız connected kurulum (secret sahibi)

Tek uç hem subscribable event'leri hem lifecycle'ı alır; type ile ayırt edersin. Katalogda ayrı bir lifecycleEvents listesi vardır (GET /plugin-meta/catalog).

Tipler

typeNe zamandataGeliştirici ne yapar
app.installedOAuth connect tamamlandı (kurulum bağlandı){ version, scopes[] }Provision — tenant kaydını oluştur/etkinleştir
subscription.activatedAbonelik aktif/trial oldu (aktivasyon + her yenileme) ⚠️ birden çok kez{ interval, currentPeriodEnd }Premium provision — idempotent (tenantId durumuna göre)
subscription.past_dueÖdeme başarısız (gecikmiş){} (boş)Degrade — premium özellikleri kısıtla
subscription.canceledAbonelik iptal edildi{ reason? }Deprovision — premium yetkiyi kapat (reason=plugin_now_free ise ücretsiz sürdür)
app.uninstalledkritikKurulum kaldırıldı{} (boş)Veri sil (GDPR/temizlik)
customer.redactkritikTenant bir müşteriyi sildi (GDPR/KVKK){ customerId, deletedAt }Bu customerId'ye ait TÜM PII'yi sil (zorunlu)
delivery.degradedCircuit breaker açıldı (endpoint art arda başarısız) — 6 saatte bir{ consecutiveDead, windowFail, windowOk, openedAt, hint }Endpoint sağlığını düzelt — teslimler geçici atlanıyor
delivery.restoredBreaker kapandı / yeniden etkinleştirildi — saatte bir (debounce){ restoredAt }Bilgi — teslimler normale döndü
delivery.disabledkritikBreaker 72 saat kesintisiz açık → webhook teslimi KALICI durduruldu{ openedAt, disabledAt, hint }Endpoint'i düzelt + yeniden etkinleştir (portal / test-emit)
delivery.throttledTeslim hacmi cap üstü, bazı teslimler düşürüldü — 6 saatte bir{ cls, count, eventType, hint }Bilgi — hacmi düşür / topluyu sadeleştir

Her tipin kendi detay sayfası var (gerçek payload + alanlar + en iyi pratik) — type'a tıkla.

app.uninstalled kritiktir: tenant kurulumu kaldırınca o tenant'a ait tüm veriyi sil (GDPR/temizlik). Bu event tek temizlik sinyalidir.
customer.redact zorunludur (GDPR/KVKK): tenant bir müşteriyi silince o customerId'ye ait tüm PII'yi sil — eklenti o tenant'ta pasif/borçlu olsa bile gelir. Critical sınıf (breaker/cap'ten muaf).
Teslim sağlığı: delivery.degraded/restored/disabled/throttled endpoint'inin teslim durumunu bildirir (circuit breaker / cap). Kurallar: Teslim Sağlığı & Politikası.
subscription.activated birden çok kez gelir: aynı abonelik için checkout + subscription.created + subscription.updated + her yenileme — her biri farklı id ile (retry değil). id ile dedup bunları birleştirmez; bu yüzden provision'ı event sayısına değil tenantId durumuna göre idempotent yap.

Mantık akışı (yaşam döngüsü)

kur → OAuth connect ────────────────────────► [app.installed]      → provision
   abonelik aktif/trial (+ her yenileme) ───► [subscription.activated] → premium aç (idempotent)*
        ├─ ödeme başarısız ─────────────────► [subscription.past_due]  → degrade (kıs)
        └─ iptal ───────────────────────────► [subscription.canceled]  → deprovision
   olay (paket) — yalnız aktif + ödenmiş ───► packet.created           → işle
kaldır ─────────────────────────────────────► [app.uninstalled]        → veri SİL (GDPR)
* activated aynı abonelik için birden çok kez (farklı id) gelir → tenantId durumuna göre idempotent

Durum makinesi: app.installed başlangıç; ödeme durumu activated ⇄ past_duearasında gidip gelebilir; canceled premium'u kapatır ama kurulum durur;app.uninstalled terminal (veri silinir).

Mimari & şema önerileri

  • Tek uç + type ile dallan: event ve lifecycle aynı webhookUrl'e düşer. switch (type) ile ayır; tanımadığın type'a güvenli (no-op + 200) yanıt ver.
  • Idempotency deposu: işlenen id'leri kalıcı sakla (örn. TTL'li tablo). Retry'da aynı id tekrar gelir → ilk satırda yut. Not: subscription.activated aynı abonelik için farklı id'lerle birden çok kez gelir — id dedup'u bunu engellemez; o aksiyonu ayrıca durum-temelli (tenantId) idempotent kur.
  • Async kuyruk: webhook'ta yalnız doğrula + kuyruğa al + hızlı 200; provision/silme gibi ağır işi worker'da yap. Geç 200 → gereksiz retry.
  • Durum makinesi modeli: tenant için installed → active ⇄ past_due → canceled → uninstalled durumu tut; her event durumu ilerletsin. Geçişleri tersine çevrilebilir kur (past_due→active).
  • currentPeriodEnd'i sakla: erişim süresinin kaynağı; interval'a tek başına güvenme.
  • Veri silme yalnız uninstalled'da: past_due/canceled veriyi KORUR (geri dönüş olabilir); app.uninstalled tek silme sinyali.
Aldığın istek (envelope) — yalnız data tipe özel
POST {webhookUrl}     // packet.created ile AYNI uç
X-Restomenum-Signature: t=<unixSec>,v1=<HMAC_SHA256(webhookSecret,"<t>.<rawBody>")>

{
  "id": "evt_…",                 // idempotency anahtarı
  "type": "app.installed",       // lifecycle tipi (aşağıdaki tablo) — event'ten BUNUNLA ayırt et
  "version": "1",
  "tenantId": "…",
  "occurredAt": 1780000000000,
  "data": { }                    // tipe göre (çoğu boş; app.installed & subscription.activated dolu)
}
Tek uçta dallanma + async worker (Node)
// /webhook — TEK uç hem event hem lifecycle alır. type ile dallan.
app.post('/webhook', express.raw({ type: '*/*' }), async (req, res) => {
  if (!verifySignature(webhookSecret, req.body, req.get('X-Restomenum-Signature'))) return res.sendStatus(401);
  const e = JSON.parse(req.body.toString('utf8'));
  if (await seen(e.id)) return res.sendStatus(200);        // idempotency: aynı id'yi yut

  await enqueue(e);     // ağır işi kuyruğa al, HEMEN 200 dön (yoksa retry tetiklenir)
  res.sendStatus(200);
});

// worker (async) — type'a göre durum makinesini ilerlet
async function handleLifecycle(e) {
  switch (e.type) {
    case 'app.installed':          await provision(e.tenantId, e.data.scopes, e.data.version); break;
    case 'subscription.activated': await enablePremium(e.tenantId, e.data.currentPeriodEnd); break; // idempotent — birden çok kez (farklı id) gelir
    case 'subscription.past_due':  await degrade(e.tenantId); break;        // sil DEĞİL, kıs
    case 'subscription.canceled':  await deprovision(e.tenantId); break;    // yetki kapat
    case 'app.uninstalled':        await purgeTenant(e.tenantId); break;    // GDPR — tenant verisi sil
    case 'customer.redact':        await redactCustomer(e.tenantId, e.data.customerId); break; // GDPR — müşteri PII sil (ZORUNLU)
    case 'delivery.disabled':      await alertOps(e); break;                // teslim kalıcı durdu → endpoint'i düzelt
    default:                       await handleEvent(e); break;             // packet.created vb. (delivery.degraded/restored/throttled bilgi)
  }
}

Güvenlik önlemleri

  • İmza zorunlu: packet.created ile aynı şema — ham gövde, install webhookSecret, ±5 dk pencere; geçersizse 401 dön, işleme.
  • Cross-tenant: her işlemde tenantId'yi doğrula — gelen veriyi yalnız o tenant'ın kaydına uygula, başka tenant'a sızdırma.
  • Idempotency: retry'da aynı id gelir → dedup; özellikle provision/deprovision/purge'ü tekrarlama.
  • Hızlı 200, async iş: ağır işi (veri silme, provision) kuyruğa al; webhook'ta bekletme.
  • app.uninstalled = veri silme: tenant verisini gerçekten temizle (GDPR); saklamak yasal risktir.
  • Secret'ı sızdırma / logla­ma: webhookSecret yalnız doğrulama için; gövdeyi loglarken PII/secret maskele.