Abonelik Uygulamalarında Mobil Reklam Atıfı: SKAN 4 ve AdAttributionKit
SKAN 4, AdAttributionKit ve ATT sonrası reklam tıklamasını ücretli aboneliğe bağlayan ölçüm hattına mühendislik rehberi: olay taksonomisi ve mutabakat desenleri.
Abonelik uygulamalarında attribution, Meta, Google veya TikTok’taki bir reklam tıklamasını veri ambarındaki doğrulanmış bir ücretli abonelik olayına bağlamak zorundadır. Install, o olaya giden yolda yalnızca ara bir sinyaldir; SKAN 4 ile ATT ise click-to-install bağlantısını install-to-subscription bağlantısından çok daha zor gözlemlenir hâle getirir. Tek bir hattın, birbirinden bağımsız saatlerle gelen üç kaynağı (SKAN postback’leri, mobil uygulama olayları, ödeme sağlayıcı makbuzları) uzlaştırması gerekir ki pazarlama, finans ve ürün aynı sayılar üzerinde çalışabilsin.
Başlangıç için sağlam varsayılan şu: ödeme sağlayıcıyı (burada RevenueCat) abonelik doğruluk kaynağı say, webhook’unu paylaşılan bir event ID altında her reklam platformuna yay ve her gece ambarda mutabakat çalıştır. SKAN 4, iOS 17.4+ üzerindeki AdAttributionKit (iOS 18.4’te genişletildi) ve sunucu taraflı conversion API’lerinin hepsi bu omurgaya asılır.
Ölçüm Problemini Doğru Çerçevelemek#
Bir abonelik uygulaması yayınlıyorsan attribution yığının, üç bağımsız saati ve üç bağımsız doğruluk kaynağı olan dağıtık bir sistemdir. Pazarlama, Meta Ads Manager’da ROAS görür. Finans, RevenueCat’te geliri görür. Ürün, ambarda kohortları görür. Sayılar nadiren eşleşir.
İş, “doğru” sayıyı seçmek değildir. İş, her tüketicinin bilinen hata payıyla kendi sinyalini aldığı bir mutabık hat kurmaktır.
Bir abonelik uygulaması için dört olay önemlidir:
first_open: install başına bir kez, istemci tarafında tetiklenirtrial_start: sunucuda doğrulanır, ürün ve deneme süresini kodlarsubscribe: denemeden sonra ilk ücretli ödeme (veya doğrudan ücretli)renewal: n’inci faturalama döngüsü, ayrı tutulur
Gölge olay churn, LTV modellemesi için döngüyü kapatır.
ATT Sonrasında Gizlilik Ortamı#
iOS’ta App Tracking Transparency onay oranları hâlâ yüzde 25 civarında; bu oran dikey sektöre ve bölgeye göre değişir (tipik olarak yüzde 25 ile 30 arası). Deterministik attribution kullanıcıların çoğu için bitti. Yerine gelen şey bir yamalı bohça:
- SKAdNetwork 4: install sonrası 0-2, 3-7 ve 8-35 günde üç postback penceresi. İnce taneli 6-bit conversion değerleri yalnızca birinci pencerede. Kaba taneli (düşük/orta/yüksek) ikinci ve üçüncü pencerelerde. Gizlilik eşikleri, küçük kampanyalarda değerleri null’a düşürür.
- AdAttributionKit, iOS 17.4 ile tanıtıldı ve iOS 18.4’te genişletildi: yapılandırılabilir attribution pencereleri, yeniden etkileşim örtüşmesi, ülke kodları ve geliştirici postback’leri. SKAN ile birlikte yaşar, kaldırma takvimi yoktur.
- Android: Google Play Install Referrer şimdilik deterministik kalıyor. Android için Privacy Sandbox çok yıllı bir geçişte.
iOS’ta hem SKAN hem AAK gerekli. Android’de Install Referrer gerekli. ATT’ye onay veren yüzde 25 için MMP veya doğrudan SDK üzerinden deterministik attribution hâlâ yakalanmaya değer.
Abonelik Uygulamaları için Olay Taksonomisi#
Temiz bir olay taksonomisi, ileride en çok baş ağrısını önleyecek tek karardır. Küçük tut, kanonik tut ve her olayın bir idempotency anahtarı olsun.
| Olay | Kaynak | Ana Parametreler | Idempotency Anahtarı |
|---|---|---|---|
first_open | İstemci SDK | install_time, source | install_id |
trial_start | RevenueCat webhook | product_id, trial_days, expected_price | original_transaction_id + “trial” |
subscribe | RevenueCat webhook | product_id, revenue_usd, currency | original_transaction_id + “subscribe” |
renewal | RevenueCat webhook | product_id, revenue_usd, period_number | transaction_id |
churn | RevenueCat webhook | reason, refund_amount | original_transaction_id + “churn” |
Kullanıcı kimliklerini reklam platformlarına göndermeden önce SHA-256 ile özetle. Install anında click ID’leri (fbclid, gclid, ttclid) deferred deep link ile yakala ve kullanıcı kaydında sakla. Click ID olmadan sunucu taraflı conversion API’leri kaba eşlemeye düşer.
Meta CAPI ile deduplication sözleşmesi katıdır: SDK ve sunucu olayları 48 saatlik pencerede aynı event_id ve event_name değerlerini paylaşmak zorundadır, aksi hâlde çift sayım olur. event_id = hash(transaction_id + event_name) kullan.
SKAN 4 Conversion Value Şeması#
İnce taneli değerler için 6 bitin var. Bu ilk 48 saatte bir kullanıcı hakkında kodlamak istediğin her şey için 64 slot demektir. Çoğu ekip bu bütçeyi boşa harcar.
Abonelik uygulamaları için işe yarayan bir şema:
- Bitler 0-2 (8 değer): funnel aşaması:
opened,onboarded,paywall_seen,trial_start,subscribe,renewal, yedek, yedek - Bitler 3-5 (8 değer): USD gelir kovası:
0,<5,5-10,10-20,20-50,50-100,100-200,200+
func encodeConversionValue(stage: FunnelStage, revenueUSD: Double) -> Int {
let stageBits = stage.rawValue & 0b111
let revenueBits = revenueBucket(for: revenueUSD) & 0b111
return (revenueBits << 3) | stageBits
}
func updateSKAN(stage: FunnelStage, revenueUSD: Double) {
let value = encodeConversionValue(stage: stage, revenueUSD: revenueUSD)
let coarse: SKAdNetwork.CoarseConversionValue =
revenueUSD >= 20 ? .high : revenueUSD >= 5 ? .medium : .low
SKAdNetwork.updatePostbackConversionValue(
value,
coarseValue: coarse,
lockWindow: false
)
}
Conversion value’yi asla düşürme. SKAN monoton artışı zorlar. trial_start aşamasından paywall_seen aşamasına geri dönersen Apple güncellemeyi sessizce düşürür.
Küçük kampanyalarda ince taneli değerler Apple’ın gizlilik eşiği yüzünden null’a düşer. Teklif stratejini kaba değerlerin üstüne kur; ince taneli değerleri de ek kazanç say.
Referans Mimari#
Cihazdan reklam platformlarına sinyali üç paralel yol taşır. Cihaz kendisi SKAN ve AAK postback’leri gönderir. MMP, onaylı kullanıcılar için deterministik attribution taşır. Backend, Meta CAPI, Google Ads API ve TikTok Events API üzerinden sunucu taraflı conversion olayları gönderir. Her yolun farklı gecikmesi, farklı doğruluğu ve farklı gizlilik dengesi vardır.
MMP vs Doğrudan SDK Entegrasyonu#
Bir Mobile Measurement Partner, ağlar arası SKAN postback’lerini toplar, onaylı kullanıcılar için deterministik attribution yapar, maliyet verilerini taşır, sahte trafiği ayıklar ve ROAS’ı birleştirir. Soru, buna ihtiyacın olup olmadığıdır.
MMP ücretlendirmesi hacimde tipik olarak install başına birkaç sent düzeyindedir ve sözleşmeyle müzakere edilir. Yüksek hacimli bir uygulama için bu gerçek paradır. Doğrudan SDK entegrasyonu ücreti kurtarır ama SKAN toplama, postback yönlendirme ve hile filtrelemeyi mühendislik ekibine yükler. Başa baş noktası install hacmine ve kaç ağ kullandığına bağlıdır.
Pratikte: RevenueCat Webhook Fan-Out#
RevenueCat, abonelik doğruluk kaynağı olarak hattın ortasında durur. Webhook’u her reklam platformuna yayılımı tetikleyen şeydir. Aşağıdaki TypeScript handler, deduplication, yetkilendirme başlığı doğrulaması ve paralel yayılımın özüdür. RevenueCat webhook’ları HMAC imza değil, Authorization başlığında paylaşılan bir Bearer token kullanır; bu yüzden kontrol basit bir string karşılaştırmasıdır.
import crypto from "node:crypto";
import type { Request, Response } from "express";
interface RCWebhook {
event: {
type: "INITIAL_PURCHASE" | "RENEWAL" | "CANCELLATION" | "EXPIRATION";
original_transaction_id: string;
product_id: string;
price_in_purchased_currency: number;
currency: string;
app_user_id: string;
purchased_at_ms: number;
period_type: "TRIAL" | "NORMAL" | "INTRO" | "PROMOTIONAL";
};
}
export async function handleRevenueCatWebhook(req: Request, res: Response) {
const authHeader = req.header("Authorization");
if (authHeader !== `Bearer ${process.env.RC_WEBHOOK_SECRET}`) {
return res.status(401).send("unauthorized");
}
const body = req.body as RCWebhook;
const { event } = body;
const eventName = mapEventName(event);
const eventId = crypto
.createHash("sha256")
.update(`${event.original_transaction_id}:${eventName}`)
.digest("hex");
const user = await loadUser(event.app_user_id);
const revenueUSD = await toUSD(event.price_in_purchased_currency, event.currency);
await Promise.allSettled([
sendMetaCAPI({ eventId, eventName, user, revenueUSD, event }),
sendGoogleAdsConversion({ eventId, eventName, user, revenueUSD }),
sendTikTokEvent({ eventId, eventName, user, revenueUSD }),
writeWarehouse({ eventId, eventName, user, revenueUSD, event }),
]);
return res.status(200).send("ok");
}
function mapEventName(event: RCWebhook["event"]): string {
if (event.type === "INITIAL_PURCHASE" && event.period_type === "TRIAL") {
return "trial_start";
}
if (event.type === "INITIAL_PURCHASE") return "subscribe";
if (event.type === "RENEWAL") return "renewal";
return "churn";
}
Gözden kaçması kolay birkaç ayrıntı: Meta CAPI’nin mobil olaylar için beklediği app_data nesnesi advertiser_tracking_enabled, application_tracking_enabled, bundle ID ve uygulama sürümünü içermelidir. Olmadan Meta kaba attribution’a düşer. Ayrıca Meta, Offline Conversions API’sini Mayıs 2025’te sonlandırdı; tüm mobil uygulama olayları artık ana Conversions API üzerinden akıyor. Google Ads tarafında Google Ads API, uygulama dönüşümleri için gclid ile sunucu taraflı yüklemeyi UploadClickConversionsRequest aracılığıyla destekler. Hash’lenmiş kullanıcı verisiyle enhanced conversions, eşleşme kalitesini artıran ayrı ve tamamlayıcı bir seçenektir.
Promise.all yerine Promise.allSettled kullan. Tek bir çalışmayan reklam ağı diğerleri için olayları düşürmemeli. Başarısızlıkları bir dead-letter queue’ya yaz ve üstel geri çekilmeyle yeniden dene.
Pratikte: StoreKit 2 Transaction Listener#
iOS’ta StoreKit 2 işlemleri JWS yükleri olarak teslim eder. Doğrulama adımı zorunludur. Doğrulanmamış olarak gelen her şey sahte kabul edilmelidir.
import StoreKit
actor TransactionListener {
func start() async {
for await result in Transaction.updates {
guard case .verified(let transaction) = result else {
continue
}
// Imzali JWS'i decode edilmis Transaction'dan degil,
// disarida duran VerificationResult'tan al.
await report(transaction: transaction, jws: result.jwsRepresentation)
await transaction.finish()
}
}
private func report(transaction: Transaction, jws: String) async {
let payload = TransactionPayload(
originalTransactionID: transaction.originalID,
productID: transaction.productID,
purchaseDate: transaction.purchaseDate,
offerType: transaction.offerType?.rawValue,
jws: jws
)
try? await BackendClient.shared.post("/transactions", payload)
}
}
offerType alanı, giriş denemesini doğrudan ücretli satın alımdan ayırt etmenin yoludur. Kritik bir ayrıntı: imzalı JWS, dış VerificationResult enum’unda jwsRepresentation olarak yaşar; decode edilmiş Transaction değerinde değil. Transaction.jsonRepresentation düz decode edilmiş JSON’dur, imzalı değildir; bunu backend’e göndermek sana hiçbir kriptografik garanti vermez. Enum’u açmadan önce result.jwsRepresentation’ı oku, sonra yüke güvenmeden önce backend’de Apple’ın açık anahtarıyla imzayı doğrula. RevenueCat kullanıyorsan bunu senin için yapar; doğrudan kuruyorsan yenileme olayları için App Store Server Notifications v2 kullan.
Öngörücü LTV ve tROAS#
Algoritmik teklif için yeterince hızlı olan tek sinyal kısa pencereli ROAS’tır. Reklam platformları teklifleri optimize etmek için 24 ila 72 saat içinde gelir olaylarına ihtiyaç duyar. Yedi günlük bir denemeden sonra gerçek ücretli dönüşümleri beklemek çok geçtir.
Çözüm öngörücü LTV. Basit bir model: deneme-ücretli dönüşüm temel oranı, beklenen yenilemelerle çarpılır, zamanla indirgenir. Gerçek ücretliyi beklemek yerine bu sayıyı trial_start üzerinde reklam platformlarına gelir olarak gönder.
Risk, geri besleme döngüsünde: reklam platformu senin tahminine göre optimize eder, tahmin zamanla sapar ve bir sonraki optimizasyon turu sapmış veriyle çalışır. Kayan 30 günlük pencerede pLTV’yi gerçek gelire karşı yeniden kalibre et ve fark yüzde 15’i aştığında uyarı ver.
Önemli Metrikler#
CPI ve CPA öncü göstergelerdir. İşin sağlığı hakkında sana hiçbir şey söylemezler. CAC, LTV, LTV:CAC oranı ve geri ödeme süresi iş metrikleridir. Sağlıklı bir tüketici abonelik uygulaması LTV:CAC değerini 3:1’in üzerinde ve geri ödemeyi 12 ayın altında hedefler. Tüketici abonelikleri için deneme-ücretli dönüşüm oranı tipik olarak yüzde 30 ile 50 arasındadır.
Harmanlanmış ROAS (ambarın söylediği) ile platformun bildirdiği ROAS (Meta’nın söylediği) arasında yüzde 20 ila 40 aralığında bir fark bekle. Bu fark yapısaldır: SKAN’ın değerleri kaba kovalara indirmesi, gizlilik eşikleri ve platformlar arası örtüşme toplanınca aşağı yukarı bu aralığı üretir. Sayıyı eritmeye çalışma; raporda ayrı bir satır olarak göster.
Yaygın Hatalar#
Benimsersen aylar kaybettirecek birkaç desen:
trial_startçift sayımı çünkü SDK ve backend paylaşılanevent_idolmadan tetiklenir. Olay ID’sini bir kez üret ve her iki yoldan geçir.- SKAN null conversion değerlerini sıfır olarak değerlendirmek. Null “gizlilik eşiği karşılanmadı” demektir, “sıfır gelir” değil. Onları açık bir
unknownkategorisine koy ve ayrı modelle. - Reklam platformlarına brüt gelir göndermek. Geliri geri beslemeden önce mağaza ücretlerini (yüzde 15 ila 30) ve beklenen iadeleri (tüketici abonelikleri için yüzde 2 ila 5) düş. Aksi hâlde tROAS teklifin fazla harcama yapar.
- Döviz çevrimini unutmak. RevenueCat USD’ye normalize eder; reklam platformları hesap para biriminde raporlayabilir. Ambarda tek bir kanonik para birimi seç.
- Saat dilimi sapması. RevenueCat UTC’dir. Meta ve Google Ads hesap saat diliminde raporlar. Olayları her zaman UTC’de sakla ve yalnızca sunum katmanında çevir.
- Doğrulanmamış App Store Server Notifications üzerine işlem yapmak. Yük ile bir şey yapmadan önce JWS imzasını doğrula.
Mutabakat: Döngüyü Kapatmak#
Finansın sayılara güvenmesini sağlayan şey, her gece çalışan mutabakat işidir. MMP attribution tablosunu RevenueCat olayları tablosuyla original_transaction_id üzerinden birleştir. Kampanya ve kohort seviyesine topla. Kayan 35 günlük SKAN düzeltme penceresini uygula. Attribute edilemeyen kovayı açıkça raporla.
İşe yarayan bir ambar modeli:
raw_mmp_attributions: install başına bir satır, attribute edilmiş kaynakraw_revenuecat_events: abonelik olayı başına bir satırreconciled_users: birleşim anahtarıapp_user_id, install kaynağını ve abonelik yaşam döngüsünü bir araya getirircohort_revenue: günlük kohort × kaynak, kayan LTV gerçekleşmeleriyle
Finans cohort_revenue’yü, pazarlama MMP panosunu, ürün de reconciled_users’ı okur. Uç durumlarda yine anlaşamayacaklar; bilinen farklar her ay yeniden tartışılmak yerine bir yere yazıldığı sürece bu yürütülebilir bir durum.
Sonuç#
Bir abonelik uygulaması için ölçüm hattı, pazarlama problemi gibi görünen bir dağıtık sistem problemidir: üç saat, üç doğruluk kaynağı ve gecikme ile doğruluk toleransları birbirinden farklı üç tüketici.
Omurgayı ödeme sağlayıcıya kuran varsayılan, abonelikler tek bir faturalama sisteminden geçtiği ve teklif verme biçimin için her gece çalışan bir ambar işi yeterince hızlı kaldığı sürece geçerlidir. Aynı anda birden fazla faturalama sisteminden satıyorsan ya da teklif verme saat içinde geri bildirim istiyorsa bu varsayılan geçerliliğini yitirir. O noktada yayılımın yeri akış tabanlı bir event bus olur ve mutabakat da gecelik olmaktan çıkıp sürekli çalışmak zorunda kalır. Hangi biçimde olursa olsun ilk kurulacak parça olay taksonomisidir; çünkü bir pano okunmaya değer hâle gelmeden önce SKAN şeması, webhook yayılımı ve ambar modeli bir olayın ne anlama geldiği konusunda anlaşmak zorundadır.
Kaynaklar#
- Apple: Receiving postbacks in multiple conversion windows (yeni sekmede açılır) - Resmi SKAN 4 postback pencere mekaniği
- Apple: AdAttributionKit and SKAdNetwork interoperability (yeni sekmede açılır) - iOS 17.4+ üzerinde SKAN ve AAK birlikte çalışması
- Apple: App Store Server Notifications v2 (yeni sekmede açılır) - Sunucu taraflı abonelik yaşam döngüsü olayları
- Apple: Meet StoreKit 2 (WWDC) (yeni sekmede açılır) - JWS tabanlı Transaction API
- Google: Play Install Referrer API (yeni sekmede açılır) - Android install attribution kaynağı
- Google Ads: Upload app conversions (yeni sekmede açılır) - Uygulamalar için enhanced conversions
- Meta: Conversions API for App Events (yeni sekmede açılır) - Sunucu taraflı mobil olay şeması
- Meta: App Events API for mobile (yeni sekmede açılır) - SDK olaylarıyla deduplication sözleşmesi
- TikTok: Events API for app (yeni sekmede açılır) - TikTok Ads için sunucu taraflı attribution
- RevenueCat: StoreKit 2 overview (yeni sekmede açılır) - JWS doğrulaması ve SDK 5.0 geçişi
- RevenueCat: Webhooks documentation (yeni sekmede açılır) - Abonelik olayı yayılım kaynağı
- AppsFlyer: SKAdNetwork solution guide (yeni sekmede açılır) - MMP tarafında SKAN toplama
- Adjust: How SKAdNetwork 4 works (yeni sekmede açılır) - Postback pencereleri ve conversion value mekaniği
- Singular: WWDC 2025 AdAttributionKit recap (yeni sekmede açılır) - iOS 18.4 AAK güncellemeleri
- Apple: User Privacy and Data Use (ATT) (yeni sekmede açılır) - App Tracking Transparency gereksinimleri
İlgili yazılar
Mobil uygulama içi satın alma kuralları, paywall kalıpları ve sunucu tarafı makbuz doğrulama ile RevenueCat entegrasyonu üzerine pratik bir rehber.
in-app-purchase · subscriptions · mobile +1
RevenueCat, Adapty, Qonversion, Apphud, Chargebee ve Stripe Billing arasında 2026'da seçim için karar çerçevesi: fiyatlandırma matematiği ve DMA etkisi.
subscriptions · payment-systems · in-app-purchase
Sunucu güdümlü arayüz, sunucu tarafı kompozisyonun mobil karşılığıdır. Zor kısmı JSON render etmek değil, eski sürümlerde ayakta kalan sürümlenmiş bileşen sözleşmesidir.
mobile · react-native · architecture
Mobil binary geri alınamaz ve eski sürümler kalıcıdır; güvenlik ve hız sunucuya kayar: BFF, tüketici güdümlü sözleşmeler ve geriye dönük uyumlu sürümleme.
mobile · api-design · testing +1
Tek bir backend üzerinde çalışan web SPA ve mobil uygulama için uzun süreli işlere dair tek bir varsayılan desen ve onu geçersiz kılmanız gereken durumlar.
api-design · real-time · webhooks +4