İçeriğe atla

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.

Ayhan Sipahi Ayhan Sipahi

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 tetiklenir
  • trial_start: sunucuda doğrulanır, ürün ve deneme süresini kodlar
  • subscribe: 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.

OlayKaynakAna ParametrelerIdempotency Anahtarı
first_openİstemci SDKinstall_time, sourceinstall_id
trial_startRevenueCat webhookproduct_id, trial_days, expected_priceoriginal_transaction_id + “trial”
subscribeRevenueCat webhookproduct_id, revenue_usd, currencyoriginal_transaction_id + “subscribe”
renewalRevenueCat webhookproduct_id, revenue_usd, period_numbertransaction_id
churnRevenueCat webhookreason, refund_amountoriginal_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#

tiklama

SKAN AAK postback

SDK olaylari

attribution

satin alma

webhook

CAPI App Events API

Reklam Platformlari Meta Google TikTok

MMP AppsFlyer Adjust

Mobil Uygulama iOS Android

Backend Event Router

RevenueCat

Ambar BigQuery Snowflake

BI ve Finans

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.

Bir veya iki

Uc veya daha fazla

Kucuk ekip

Buyuk ekip

Kati AB

Esnek

Evet

Hayir

Kac reklam agi?

Dogrudan SDK entegrasyonu

Muhendislik kapasitesi?

MMP kullan

Veri ikametgah kisitlari?

Adjust veya dogrudan SDK

Birlesik maliyet ve kreatif?

Singular

AppsFlyer veya Adjust

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 oncu

CPA oncu

CAC is metrigi

LTV is metrigi

LTV:CAC > 3:1

Geri odeme < 12ay

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şılan event_id olmadan 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 unknown kategorisine 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ş kaynak
  • raw_revenuecat_events: abonelik olayı başına bir satır
  • reconciled_users: birleşim anahtarı app_user_id, install kaynağını ve abonelik yaşam döngüsünü bir araya getirir
  • cohort_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#

İlgili yazılar