Skip to content
~/sph.sh

Döngüyü Kapatmak: Mobil Uygulamalarda Reklam Harcamasından Ücretli Aboneye

SKAN 4, AdAttributionKit ve ATT sonrası gizlilik döneminde reklam tıklamasını ücretli aboneliğe bağlayan ölçüm hattı için mühendislik rehberi. Olay taksonomisi, referans mimari ve mutabakat desenleri.

Özet

Mobil attribution rehberlerinin çoğu install olayında durur. Abonelik uygulamaları install üzerinden para kazanmaz. Para, ücretliye dönen denemelerden ve yenilenen ücretli kullanıcılardan gelir. Bu yazı, Meta, Google veya TikTok'taki bir reklam tıklamasını veri ambarındaki doğrulanmış ücretli abonelik olayına bağlayan ölçüm hattının baştan sona incelemesidir. SKAN 4, iOS 17.4+ üzerinde AdAttributionKit (iOS 18.4 ile genişletildi), sunucu taraflı conversion API'leri, doğruluk kaynağı olarak RevenueCat ve finans, pazarlama ve ürünün sayılarda uzlaşmasını sağlayan mutabakat işini kapsar.

Ö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ı Gizlilik Manzarası

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 kampanyaları boşaltı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 acıyı ö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) gecikmeli derin bağlantı 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+
swift
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 tarafından boşaltılır. Teklif stratejini kaba değerleri temel alarak kur, istisna gibi değil.

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, hile filtreler 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.

Uygulama: 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.

typescript
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.

Uygulama: 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.

swift
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üdür. Reklam platformu tahmininize göre optimize eder. Tahmininiz sapar. Sapmış veriye göre optimize edersiniz. Yuvarlanan 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. Fark bir hata değildir. SKAN kaba kovalama, gizlilik eşikleri ve platformlar arası örtüşmenin toplamıdır. Gizleme, raporla.

Yapılmaması Gerekenler

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 gece yarısı mutabakat işidir. MMP attribution tablosunu RevenueCat olayları tablosuyla original_transaction_id üzerinden birleştir. Kampanya ve kohort seviyesine yuvarla. Yuvarlanan 35 günlük SKAN ayar penceresini uygula. Attribute edilmeyen 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, yuvarlanan LTV gerçekleriyle

Finans cohort_revenue'yü okur. Pazarlama MMP panosunu okur. Ürün reconciled_users'ı okur. Uç durumlarda hâlâ anlaşamayacaklar. Bu normal. Bilinen farkları belgele ve devam et.

Sonuç

Bir abonelik uygulaması için ölçüm hattı, pazarlama problemi gibi görünen bir dağıtık sistem problemidir. Üç saatin, üç doğruluk kaynağın ve gecikme ile doğruluk için farklı toleransları olan üç tüketicin var. İş "gerçek" sayıyı bulmak değildir. İş, her tüketicinin bilinen hata payıyla sinyal aldığı ve SKAN şeması, webhook yayılımı ve ambar modelinin bir olayın ne anlama geldiği konusunda anlaştığı bir mutabık hat kurmaktır.

Olay taksonomisiyle başla. Deduplication'ı doğru yap. Sonra SKAN, CAPI ve mutabakatı üzerine ekle. Taksonomiyi atlayıp doğrudan panolara geçen ekipler sonraki yılı eşleşmeyen sayıları ayıklayarak geçirir.

Kaynaklar

İlgili Yazılar