09 — Webhook Yönetimi ve Olaylar
Kimin için: Asenkron veri senkronizasyonu, otomatik lisans açma/kapama ve olay dinleme yapan geliştiriciler.
Ön koşul: Genel ağdan erişilebilir bir HTTP POST uç noktası (https://...).
Tahmini süre: 20 dakika.
Ne Öğreneceksiniz
- TALOST ID Webhook mimarisini ve olay kataloğunu,
X-Talost-Signaturebaşlığındaki HMAC-SHA256 imzasını ve zaman damgasını doğrulamayı,- Yeniden deneme (Retry & Exponential Backoff) ve mükerrer işlem engellemeyi (Idempotency).
Webhook Mimarisi ve Teslimat Döngüsü
Kullanıcı abonelik satın aldığında, iptal ettiğinde veya platform bakıma girdiğinde, TALOST ID uygulamanızın kayıtlı webhook adresine bir POST isteği gönderir:
sequenceDiagram
autonumber
participant Talost as TALOST ID Webhook Kuyruğu
participant App as Uygulamanız (/api/talost/webhook)
participant DB as Uygulama Veritabanınız
Talost->>App: POST /api/talost/webhook (X-Talost-Signature: t=...,v1=...)
App->>App: İmza ve Zaman Damgası Kontrolü (HMAC-SHA256)
App->>DB: evt_id daha önce işlendi mi? (Idempotency)
alt Daha önce işlenmedi
App->>DB: Kullanıcı yetkilerini güncelle (subscription.activated)
App-->>Talost: HTTP 200 OK
else Zaten işlendi
App-->>Talost: HTTP 200 OK (Mükerrer istek yutulur)
end
Olay Kataloğu
Olay Tipi (type) |
Kategori | Açıklama |
|---|---|---|
subscription.activated |
Abonelik | Yeni bir abonelik başlatıldı. Kullanıcıya lisans hakkı tanımlanmalıdır. |
subscription.renewed |
Abonelik | Dönemlik abonelik yenilendi. Bitiş tarihi uzatılmalıdır. |
subscription.canceled |
Abonelik | Kullanıcı aboneliğini iptal etti (Dönem sonunda bitecek). |
subscription.expired |
Abonelik | Abonelik süresi doldu. Erişim hakları kapatılmalıdır. |
subscription.plan_changed |
Abonelik | Plan yükseltildi veya düşürüldü. Yeni plan limitleri uygulanmalıdır. |
payment.succeeded |
Ödeme | Ödeme başarıyla tahsil edildi. |
payment.refunded |
Ödeme | Ödeme iade edildi. |
user.updated |
Kullanıcı | Kullanıcı profil detayları güncellendi. |
user.suspended |
Kullanıcı | Kullanıcı hesabı askıya alındı. Oturumu derhal kapatılmalıdır. |
user.deleted |
Kullanıcı | Kullanıcı hesabı silindi (KVKK/GDPR gereği verileri temizlenmelidir). |
session.revoked |
Oturum | Kullanıcının tüm oturumları merkezi olarak sonlandırıldı. |
seat.assigned |
Koltuk | Kurumsal organizasyonda kullanıcıya koltuk lisansı atandı. |
seat.revoked |
Koltuk | Kullanıcıdan koltuk lisansı geri alındı. |
app.maintenance.started |
Bakım | Uygulama bakım moduna girdi. |
app.maintenance.ended |
Bakım | Uygulama bakımdan çıktı. |
webhook.test |
Teşhis | Panelden gönderilen test ping isteği. |
İmza Doğrulama Algoritması
Her istekte X-Talost-Signature başlığı gönderilir:
X-Talost-Signature: t=1728000000,v1=5d41402abc4b2a76b9719d911017c592...
- Başlık parçalanır:
t(Unix zaman damgası) vev1(64 karakterlik hex HMAC). - Replay Attack Koruması:
Math.abs(Date.now() / 1000 - t) > 300ise (5 dakikadan eski istekler) reddedilir. - İmza Hesabı:
HMAC_SHA256(webhook_secret, t + "." + rawBody)hesaplanır. - Sabit Zamanlı Karşılaştırma: Zamanlama saldırılarını (timing attack) önlemek için
crypto.timingSafeEqualile karşılaştırılır.
İmzayı doğrularken kesinlikle `JSON.parse` edilmiş nesneyi değil, **istekle gelen ham baytları (raw string/buffer)** kullanmalısınız. Aksi takdirde JSON anahtar sıralaması veya boşluk farkları yüzünden imza doğrulaması başarısız olur.
Kod Örnekleri
1. Next.js App Router (TypeScript)
// app/api/talost/webhook/route.ts
import { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";
export async function POST(req: NextRequest) {
const signatureHeader = req.headers.get("x-talost-signature");
if (!signatureHeader) {
return NextResponse.json({ error: "İmza başlığı eksik." }, { status: 400 });
}
// 1. Ham gövdeyi metin olarak al (Raw Body)
const rawBody = await req.text();
// 2. Başlığı ayrıştır (t=...,v1=...)
const parts = Object.fromEntries(
signatureHeader.split(",").map((p) => p.trim().split("="))
);
const timestamp = parseInt(parts.t, 10);
const receivedSig = parts.v1;
// 3. Zaman damgası toleransı (300 saniye)
const now = Math.floor(Date.now() / 1000);
if (isNaN(timestamp) || Math.abs(now - timestamp) > 300) {
return NextResponse.json({ error: "Zaman aşımı (Olası tekrar saldırısı)." }, { status: 400 });
}
// 4. İmzayı hesapla ve sabit zamanlı karşılaştır
const secret = process.env.TALOST_WEBHOOK_SECRET!; // whsec_...
const expectedSig = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const sigBufferA = Buffer.from(receivedSig || "", "hex");
const sigBufferB = Buffer.from(expectedSig, "hex");
if (sigBufferA.length !== sigBufferB.length || !crypto.timingSafeEqual(sigBufferA, sigBufferB)) {
return NextResponse.json({ error: "İmza doğrulaması başarısız." }, { status: 401 });
}
// 5. Olayı işle (Idempotency için event.id kaydedilmeli)
const event = JSON.parse(rawBody);
switch (event.type) {
case "subscription.activated":
case "subscription.renewed":
console.log(`Kullanıcı ${event.data.user_id} için ${event.data.plan_code} planı aktif edildi.`);
// Veritabanınızda kullanıcı haklarını açın
break;
case "subscription.expired":
console.log(`Kullanıcı ${event.data.user_id} aboneliği doldu.`);
// Hakları kapatın
break;
case "user.deleted":
console.log(`Kullanıcı ${event.data.user_id} silindi.`);
// KVKK gereği kullanıcı verilerini anonimleştirin veya silin
break;
default:
console.log(`İşlenmeyen olay: ${event.type}`);
}
// TALOST ID'ye başarı teyidi dön (HTTP 200)
return NextResponse.json({ received: true });
}
Teslimat Kuralları ve Yeniden Deneme (Retry)
Uygulamanız HTTP 200-299 dışında bir kod dönerse veya 10 saniye içinde yanıt vermezse, TALOST ID isteği aşağıdaki aralıklarla 5 kez tekrar dener:
- 1. Deneme: 1 dakika sonra
- 2. Deneme: 5 dakika sonra
- 3. Deneme: 30 dakika sonra
- 4. Deneme: 2 saat sonra
- 5. Deneme: 12 saat sonra
5 deneme sonunda başarısız olan teslimatlar Geliştirici Portalında ve Admin Konsolunda Başarısız olarak loglanır ve manuel olarak tekrar gönderilebilir.
İlgili Sayfalar
Son doğrulama: 2026-10-04 · Kaynak: _envanter.md, src/lib/webhooks/signature.ts, src/lib/webhooks/events.ts