İçeriğe geç
codebiy
← Yazı masasına dön

App Store Connect salesReports, rapor saati gelmeden “satış yok” dedi

Pasifik saatiyle 04:24'te Apple, raporunu doksan dakika içinde yayımlayacağı bir gün için “satış yok” dedi. İlk yanıtı kaydeden uygulama o günü gizler. Bizimki bu yüzden rapor saati gelene kadar bekliyor.

Tillroll, geliştiricinin günlük App Store satış raporlarını, yine onun kendi App Store Connect API anahtarıyla cihazda okuyan iPhone ve iPad uygulamamız. Ekim 2026'nın başında hâlâ geliştirme aşamasındaydı. Tasarım belgesini yazdığımız gün, 6 Ekim 2026'da, Apple'ın satış raporları endpoint'ini gerçek bir anahtarla, tek bir geliştirici hesabı (kendi hesabımız) için çağırdık ve aldığımız her yanıtı not ettik.

Apple'ın endpoint sayfasında beş durum kodu var: 200, 400, 401, 403 ve 429. Biz bunların yanında 404, 410 ve 500 de aldık. 404 ise yalnızca durum koduna bakarak ayırt edilemeyen iki ayrı anlama geliyordu. Eşitlemeyi buna göre kurduk: “satış yok” yanıtına ancak o günün rapor saati geçince inanıyor, sonra da üç gün daha sorguluyor.

Aşağıdakilerin hepsi, o tek hesap için Ekim 2026 başında aldığımız yanıtlar. Apple bunların herhangi birini değiştirebilir. Bu yüzden yazıyı kesin bilgi diye değil, test edilecekler listesi diye okuyun.

Ücretli indirmenin gerçek zamanlı bildirimi yok

App Store Server Notifications, uygulama içi satın almaları ve abonelikleri gerçekleştiği anda bildiriyor. Ekim 2026'da listelenen 23 bildirim tipinden hiçbiri, uygulamanın kendisi satın alındığında ya da indirildiğinde gönderilmiyor. Bunlar için tek kaynak günlük Summary Sales Report: her gün için GET /v1/salesReports endpoint'ine bir istek. İstekte filter[frequency]=DAILY, filter[reportType]=SALES, filter[reportSubType]=SUMMARY, rapor tarihi ve satıcı numarası (vendor number) bulunuyor.

Hiçbir satış takip uygulaması bundan fazlasını vadedemez. Zaman birimi gün, o gün de ertesi sabah geliyor. Tillroll'un sayfasında bu açıkça yazıyor, çünkü ücretli indirmeler için canlı rakam vadeden bir uygulama bu endpoint'in veremeyeceği bir şeyi vadetmiş olur.

Rapor günü, Pasifik saatine göre bir tarihtir

Apple'ın raporların ne zaman hazır olduğunu anlatan sayfasına göre günlük raporlar ertesi gün, genellikle Pasifik saatiyle 08:00'e kadar hazır oluyor. Bu, yazın 15:00 UTC, kışın 16:00 UTC, İstanbul'da ise 18:00 ve 19:00 demek. Dolayısıyla raporu olabilecek en yeni gün, telefon nerede olursa olsun, Pasifik saatine göre dündür.

Rapor günü bir an değil, bir etikettir. Bu yüzden onu saat dilimi olmadan, üç sayı olarak saklıyoruz: yıl, ay, gün. Günü ekrana yazmak gerektiğinde, cihaz hangi takvimi kullanırsa kullansın, UTC'ye göre o günün öğle saatini alıp miladi takvimle biçimlendiriyoruz. Kaliforniya'dan çok uzaktaki ya da yılları başka türlü sayan bir takvime ayarlı telefon da Apple'ın kastettiği günü yazıyor. Raporun ne zaman beklendiği ise ayrı bir değer: ertesi gün, Pasifik saatiyle 08:00.

“Satış yok” ile “henüz yayımlanmadı” aynı 404'le dönüyor

“Henüz yayımlanmadı” ile “o gün satış yok” aynı yanıt: 404 durum kodu, NOT_FOUND hata kodu. Yalnızca detail alanındaki cümle farklı. Boş bir gün için There were no sales for the date specified. yanıtı geldi. Henüz yaşanmamış bir gün için gelen cümle ise Report is not available yet. Daily reports for the Americas are available by 5 am Pacific Time; diye başlıyor ve öteki bölgelerle sürüyor.

Sonra ikisi birbirine karıştı. 6 Ekim'de Pasifik saatiyle 04:24'te Apple, 5 Ekim için “satış yok” (“There were no sales”) dedi. Apple'ın rapor saatleri sayfasındaki 08:00'e üç saatten fazla, kendi hata cümlesinde Amerika kıtası için yazan 05:00'e ise 36 dakika vardı. 05:18'de komut satırı aracımız o gün için hâlâ rapor bulamıyordu. 05:53'e geldiğimizde ise sonraki bir çalıştırmada rapor inmişti: Apple, 5 Ekim'in raporunu yayımlamıştı.

İllüstrasyon: evrak tepsisi boş, arkasında yuvarlak bir masa saati var, kırmızı zarf ise taşıma bandında, tepsiye henüz ulaşmamış. Rapor saatinden önce gelen “satış yok” yanıtı, “henüz yayımlanmadı” demek olabilir.

Demek ki cümleye, rapor saatinden önce güvenilemez. İstemci bu yüzden yalnızca Apple'ın ne dediğini aktarıyor. Örnekte error, Apple'ın JSON hata gövdesindeki ilk kayıt. detail de onun cümlesi, gövde JSON değilse gövdenin başı:

switch response.statusCode {
// Both 404s share one error code; only the sentence differs. Anything
// unrecognised counts as "not yet", because a day wrongly stored as empty
// would hide its sales. Only Apple's own JSON can say "no sales".
case 404 where error?.detail?
    .localizedCaseInsensitiveContains("no sales") == true:
    return .noSales
case 404: return .notAvailableYet
case 410: return .gone
case 401: throw SalesAPIError.unauthorized
case 403 where error?.code?.contains("AGREEMENT") == true:
    throw SalesAPIError.agreementRequired
case 403: throw SalesAPIError.forbidden
case 429: throw SalesAPIError.rateLimited
case 400 where error?.source?.parameter == "filter[vendorNumber]":
    throw SalesAPIError.invalidVendorNumber
default:
    throw SalesAPIError.unexpected(status: response.statusCode, detail: detail)
}

Asimetriyi bilerek kurduk: yanlışlıkla “henüz değil” sayılan bir günün bedeli, sonraki eşitlemede yalnızca bir istek daha. 400 dalı, Apple'ın sayfasında yazan durumu karşılıyor. Uydurma bir satıcı numarasıyla ise 500 aldık. Bu yanıt default dalına düşüyor. Uygulama o zaman bağlantı ekranında, Apple'ın sunucu hatası verdiğini ve önce satıcı numarasına bakılması gerektiğini söylüyor.

Eşitleme “satış yok” yanıtına ancak rapor saati geçince inanıyor

Eşitleme en yeni günden geriye doğru gün gün ilerliyor ve üç karara dayanıyor:

  1. “Satış yok” yanıtına ancak o günün raporunun beklendiği saat geçtikten sonra inanılır. Öncesinde “henüz yayımlanmadı” sayılır.
  2. Boş diye kaydedilen gün, “satış yok” yanıtı rapor saatinden en az üç gün sonra doğrulanana kadar her eşitlemede yeniden sorgulanır.
  3. Apple'ın artık sunmadığı bir gün, taramayı hata vermeden bitirir.

“Satış yok” yanıtının yalanlandığını bir kez gördük: 5 Ekim günü için, o da rapor saatinden önce. Rapor saatinden sonra yalanlandığını görmedik. Yani üç gün, bizim seçtiğimiz bir güvenlik payı.

Apple Developer Forums'taki 2018 tarihli bir başlıkta bir geliştirici, bir abone raporunu anlatıyor: o gün abonelik olayları olduğu hâlde Apple'ın eski Reporter aracı hep aynı cümleyle yanıt vermiş. Geliştirici, ekim sonundan itibaren her sabah yeniden denemiş. Rapor kasımda bir sabah inmiş. Bu, raporun beklendiği günden en az beş gün sonrası, yani bizim güvenlik payımızdan uzun.

Aşağıdaki döngüde latest, raporu olabilecek en yeni gün; days ise pencerenin genişliği. known her gün için zaten saklanmış olanı, moment eşitlemenin başladığı anı, date.expectedBy ertesi gün Pasifik saatiyle 08:00'i, settleTime da üç günü gösteriyor.

walk: for date in (latest.advanced(by: 1 - max(1, days))...latest).reversed() {
    let recheck = known[date] == .noSales
        && (store.noSalesConfirmed(for: date) ?? .distantPast)
            < date.expectedBy.addingTimeInterval(Self.settleTime)
    guard known[date] == nil || recheck else { continue }
    switch try await fetch(date) {
    case .report(let file):
        try store.save(report: file, for: date)
        summary.arrived.append(date)
    // Before the report is due, "no sales" may only mean that Apple has not
    // generated it yet.
    case .noSales where moment >= date.expectedBy:
        try store.saveNoSales(for: date, confirmedAt: moment)
        if known[date] == nil { summary.empty.append(date) }
    case .noSales, .notAvailableYet:
        // A day already stored as empty stays what the store says it is.
        if known[date] == nil { summary.unpublished.append(date) }
    case .gone:
        break walk
    }
}

HTTP oturumu cache de çerez de tutmuyor. Böylece telefon eski bir “henüz yayımlanmadı” yanıtını kendi kendine yinelemiyor. Taramanın kesin bir sınırı da var. Pasifik saatine göre bugünden 365 gün önceki gün için yanıt hâlâ geliyordu, ondan bir gün öncesi ise 410 ve GONE_ERROR ile yanıtlandı. Bir yıl geriye giden eşitleme bunu geçmişin sonu sayıyor ve hata bildirmiyor.

Anahtarın rolü, anahtarın tipi, satıcı numarası ve Accept header'ı

İlk üçü ilk isteği durdurabilir. Dördüncüsü, istemcimizdeki hiç test etmediğimiz bir varsayım.

Konu İsteği ne durdurur Nereden biliyoruz
Anahtarın rolü Yalnızca Sales erişimi olan anahtar 403 alıyor. Sales and Reports, Finance ya da Admin gerekiyor İlk anahtarımız reddedildi
Anahtarın tipi Ekip anahtarı olmalı. Bireysel anahtar satış raporlarına ulaşamıyor Apple'ın API anahtarları sayfası
Satıcı numarası Onu döndüren bir endpoint bulamadık. Uydurma bir numara, sayfada yazan 400 yerine 500 aldı Kendi isteğimiz
Accept header'ı İstemcimiz application/a-gzip gönderiyor, başka her değer için 406 bekliyor Kayıtlı bir testimiz yok

Anahtarla ilgili bu kural, koddan çok kurulum ekranını şekillendirdi. Anahtar oluşturma penceresinde erişim düzeyinin adı “Sales and Reports”. Yalnızca Sales erişimi olan bir anahtar uygulamaları listeleyebiliyor, yani ilk rapor isteği reddedilene kadar sağlam görünüyor. Kurulum rehberinde bu yüzden seçilecek sözcükler harfi harfine yazıyor.

Apple, analitik raporlarını indirmeyi anlatan sayfasında üçüncü bir tarafa verilecek anahtar için Sales and Reports rolünü öneriyor: bu rol salesReports'a ulaşıyor, finans raporlarına ulaşmıyor. Bu da telefonda duran bir anahtara uygun düşüyor.

Satıcı numarasını elle kopyalamak gerekiyor. Apple'ın ödemeler ve gelirler sayfasına göre numara sol üst köşede, tüzel kişilik adının altında duruyor. Anahtar telefona girdikten sonra onu kaybetmemek için gerekenleri ise kilit ilk kez açılmadan önce UserDefaults'un nasıl davrandığını anlatan yazımızda bulabilirsiniz.

Rapor dosyası: sütunlar adıyla, tutar birim başına

Dosya, başlık satırı olan, gzip ile sıkıştırılmış, sekmeyle ayrılmış bir metin. Sütun sırası Apple'ın sütun tablosundakinden farklı. Bize gelen dosyalarda Device sütunu Supported Platforms'tan, Proceeds Reason da Preserved Pricing'den önce geliyor. Tabloda iki çift de ters sırada.

Parser bu yüzden her sütunu başlık satırındaki adıyla buluyor. Bu, Apple'ın ileride ekleyeceği sütunları da karşılıyor.

filter[version] göndermiyoruz: endpoint sayfasında bu parametre isteğe bağlı görünüyor ve 6 Ekim 2026'da rapor, parametre olmadan da geldi. Aynı sayfada bu rapor için tek sürüm olarak 1_0 yazıyor, sütun tablosunun başlığında ise “Version 1_3” var. Bize hangi sürümün geldiğini bilmiyoruz.

“Developer Proceeds”, satırın kendi “Currency of Proceeds” para biriminde, birim başına bir tutar. Satırın toplamı, adet çarpı bu tutar; iade satırında adet eksi, birim başına tutar artı. Hepsi Apple'ın tahmini, hesaba yatan ödeme değil.

Bir günün satırları birçok para biriminde gelir. Farklı para birimlerindeki tutarları asla toplamıyoruz: toplamları her para birimi için ayrı, ondalık sayı olarak tutuyoruz. Gösterirken en yeni kur tablosuyla çeviriyor ve başına “≈” koyuyoruz.

Ham dosyayı saklamak

Her günün dosyasını Apple'ın gönderdiği hâliyle saklıyoruz ve geri kalan her şeyi ondan türetiyoruz. Sonraki bir sürüm yeniden indirmeden daha fazla sütun okuyabilir, migration gerektiren bir şema da yok. Dosya ancak açılabiliyorsa ve başlık satırında parser'ın ihtiyaç duyduğu sütunlar varsa saklanıyor. Böylece 200 durum koduyla gelen bir HTML hata sayfası diske hiç ulaşmıyor. Bu sürümün okuyamadığı tek bir satır ise dosyanın saklanmasına engel olmuyor, çünkü bir yıl sonra o dosya artık indirilemiyor.

Satış raporları endpoint'i için kontrol listesi

  • Dokümantasyona göre tasarlamadan önce kendi hesabınızın gerçekte ne döndürdüğünü test edin.
  • Rapor gününü Pasifik saatine göre bir tarih sayın, raporun ne zaman beklendiğini ayrıca hesaplayın.
  • “Satış yok” yanıtına ancak rapor saati geçtikten sonra inanın ve birkaç gün daha sorgulayın.
  • Tanımadığınız her yanıtı “boş” diye değil, “henüz değil” diye kaydedin.
  • Bu istekler için HTTP cache'i kapatın.
  • Sütunları adıyla okuyun, ham dosyayı saklayın ve iki para birimini asla toplamayın.
OKUMAYA DEVAM ETiOS'te UserDefaults boş dönüyorsa bu ilk açılış demek değildir ↗Expo OTA güncellemesi eski build'leri çökertti: lazy import yetmedi ↗