# App Store Connect API 2026: TestFlight, Submission, Analytics Otomasyonu
App Store Connect API, Apple'ın App Store operasyonlarını programatik olarak yönetmek için sunduğu REST API. 2018'de ilk versiyonu çıktı, 2026 itibarıyla v3.8'e ulaştı ve artık neredeyse tüm App Store Connect web arayüzü işlemlerini kapsıyor. Fastlane spaceship üzerinden yıllarca yürüttüğün otomasyonları artık native Swift ile yapabilirsin — third-party dependency, session cookie hack, eski Apple API workaround yok.
💡 Pro Tip: Fastlane kullanıyorsan migration'ı aceleye getirme. ASC API bazı işlevlerde hâlâ eksik (örn. App Clips entitlements yönetimi). Ama yeni pipeline kuruyorsan doğrudan ASC API ile başla — 2026'da spaceship tabanlı Fastlane action'larının yarısı deprecated.
İçindekiler
- JWT Authentication: ES256 Token Üretimi
- Core Endpoints: Apps, Builds, Beta Groups
- Upload API: TUS Protocol ile Build Yükleme
- TestFlight Otomasyonu
- Metadata ve Submission Automation
- In-App Purchase ve Subscription Yönetimi
- Analytics ve Sales Reports
- Fastlane Spaceship Karşılaştırması
- CI/CD Integration Patterns
- Rate Limits ve Error Handling
JWT Authentication: ES256 Token Üretimi
ASC API, OAuth veya API key kullanmıyor — JWT (JSON Web Token) ile ES256 imzalama yapıyor. Her request için 20 dakika geçerli token üretmen gerekiyor.
API Key Oluşturma
App Store Connect Dashboard → Users and Access → Keys → Generate API Key. Şu bilgileri al:
- Issuer ID:: UUID formatında, account'a özel
- Key ID:: Kısa alfanumerik kod
- Private Key (.p8):: Sadece bir kez indirilir, kaybedersen yenisi lazım
Swift ile JWT Token Üretimi
swift
1import Foundation2import CryptoKit3 4struct AscJwtGenerator {5 let issuerId: String6 let keyId: String7 let privateKeyPem: String8 9 func generateToken(expirationMinutes: Int = 20) throws -> String {10 // Header11 let header = ["alg": "ES256", "kid": keyId, "typ": "JWT"]12 let headerData = try JSONSerialization.data(withJSONObject: header)13 let headerEncoded = headerData.base64URLEncoded14 15 // Payload16 let now = Date()17 let exp = now.addingTimeInterval(TimeInterval(expirationMinutes * 60))18 let payload: [String: Any] = [19 "iss": issuerId,20 "iat": Int(now.timeIntervalSince1970),21 "exp": Int(exp.timeIntervalSince1970),22 "aud": "appstoreconnect-v1"23 ]24 let payloadData = try JSONSerialization.data(withJSONObject: payload)25 let payloadEncoded = payloadData.base64URLEncoded26 27 // Signing28 let signingInput = "(headerEncoded).(payloadEncoded)"29 let signingData = Data(signingInput.utf8)30 31 let privateKey = try P256.Signing.PrivateKey(pemRepresentation: privateKeyPem)32 let signature = try privateKey.signature(for: signingData)33 let signatureEncoded = signature.derRepresentation.base64URLEncoded34 35 return "(signingInput).(signatureEncoded)"36 }37}38 39// Data extension — base64 URL encoding40extension Data {41 var base64URLEncoded: String {42 base64EncodedString()43 .replacingOccurrences(of: "+", with: "-")44 .replacingOccurrences(of: "/", with: "_")45 .replacingOccurrences(of: "=", with: "")46 }47}Token Caching ve Yenileme
swift
1actor TokenCache {2 private var cachedToken: String?3 private var expiresAt: Date = .distantPast4 5 func validToken(generator: AscJwtGenerator) async throws -> String {6 if let token = cachedToken, Date() < expiresAt.addingTimeInterval(-60) {7 return token8 }9 let newToken = try generator.generateToken(expirationMinutes: 20)10 cachedToken = newToken11 expiresAt = Date().addingTimeInterval(20 * 60)12 return newToken13 }14}Core Endpoints: Apps, Builds, Beta Groups
App Listesi ve Bundle ID Lookup
swift
1struct AscClient {2 let baseURL = URL(string: "https://api.appstoreconnect.apple.com/v1")!3 let tokenCache: TokenCache4 let generator: AscJwtGenerator5 6 func fetchApps() async throws -> [AscApp] {7 let token = try await tokenCache.validToken(generator: generator)8 var request = URLRequest(url: baseURL.appendingPathComponent("apps"))9 request.setValue("Bearer (token)", forHTTPHeaderField: "Authorization")10 11 let (data, response) = try await URLSession.shared.data(for: request)12 guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {13 throw AscError.httpError((response as? HTTPURLResponse)?.statusCode ?? 0)14 }15 let decoded = try JSONDecoder().decode(AscAppsResponse.self, from: data)16 return decoded.data17 }18 19 func fetchBuilds(appId: String, limit: Int = 25) async throws -> [AscBuild] {20 let token = try await tokenCache.validToken(generator: generator)21 var components = URLComponents(22 url: baseURL.appendingPathComponent("builds"),23 resolvingAgainstBaseURL: false24 )!25 components.queryItems = [26 URLQueryItem(name: "filter[app]", value: appId),27 URLQueryItem(name: "limit", value: "(limit)"),28 URLQueryItem(name: "sort", value: "-uploadedDate"),29 URLQueryItem(name: "include", value: "app,preReleaseVersion")30 ]31 32 var request = URLRequest(url: components.url!)33 request.setValue("Bearer (token)", forHTTPHeaderField: "Authorization")34 35 let (data, _) = try await URLSession.shared.data(for: request)36 let decoded = try JSONDecoder().decode(AscBuildsResponse.self, from: data)37 return decoded.data38 }39}Beta Groups Yönetimi
swift
1extension AscClient {2 // Beta group'a tester ekle3 func addTesterToBetaGroup(4 groupId: String,5 email: String,6 firstName: String,7 lastName: String8 ) async throws {9 let token = try await tokenCache.validToken(generator: generator)10 11 // Önce beta tester oluştur12 let testerBody: [String: Any] = [13 "data": [14 "type": "betaTesters",15 "attributes": [16 "email": email,17 "firstName": firstName,18 "lastName": lastName19 ],20 "relationships": [21 "betaGroups": [22 "data": [["type": "betaGroups", "id": groupId]]23 ]24 ]25 ]26 ]27 28 var request = URLRequest(url: baseURL.appendingPathComponent("betaTesters"))29 request.httpMethod = "POST"30 request.setValue("Bearer (token)", forHTTPHeaderField: "Authorization")31 request.setValue("application/json", forHTTPHeaderField: "Content-Type")32 request.httpBody = try JSONSerialization.data(withJSONObject: testerBody)33 34 let (_, response) = try await URLSession.shared.data(for: request)35 guard let http = response as? HTTPURLResponse, http.statusCode == 201 else {36 throw AscError.testerCreationFailed37 }38 }39 40 // Build'i beta group'a assign et41 func addBuildToBetaGroup(buildId: String, groupId: String) async throws {42 let token = try await tokenCache.validToken(generator: generator)43 let body: [String: Any] = [44 "data": [["type": "builds", "id": buildId]]45 ]46 let url = baseURL47 .appendingPathComponent("betaGroups")48 .appendingPathComponent(groupId)49 .appendingPathComponent("relationships")50 .appendingPathComponent("builds")51 52 var request = URLRequest(url: url)53 request.httpMethod = "POST"54 request.setValue("Bearer (token)", forHTTPHeaderField: "Authorization")55 request.setValue("application/json", forHTTPHeaderField: "Content-Type")56 request.httpBody = try JSONSerialization.data(withJSONObject: body)57 58 let (_, response) = try await URLSession.shared.data(for: request)59 guard let http = response as? HTTPURLResponse, http.statusCode == 204 else {60 throw AscError.buildAssignmentFailed61 }62 }63}Upload API: TUS Protocol ile Build Yükleme
Build yükleme — .ipa dosyasını App Store Connect'e göndermek — TUS (resumable upload) protokolü kullanıyor. Apple'ın eski altool ve iTMSTransporter araçları hâlâ çalışıyor ama Upload API'yi direkt kullanmak daha temiz.
iTMSTransporter vs altool vs Upload API
Araç | Durum 2026 | Avantaj | Dezavantaj |
|---|---|---|---|
`altool` | Deprecated | Tanıdık | 2025'te kaldırıldı |
`iTMSTransporter` | Aktif | Güvenilir | Binary, platform-specific |
Upload API (TUS) | Aktif | Programatik, resumable | Karmaşık implementation |
`xcodebuild -exportArchive` + notary | Aktif | Native | CI/CD entegrasyonu gerekir |
TUS Upload Flow
swift
1struct TusUploader {2 let ascClient: AscClient3 4 // 1. Upload URL al5 func reserveUploadUrl(appId: String, fileName: String, fileSize: Int) async throws -> String {6 let token = try await ascClient.tokenCache.validToken(generator: ascClient.generator)7 let body: [String: Any] = [8 "data": [9 "type": "appStoreVersionSubmissions",10 "attributes": [11 "fileName": fileName,12 "fileSize": fileSize13 ]14 ]15 ]16 17 var request = URLRequest(18 url: ascClient.baseURL.appendingPathComponent("bundleIds")19 )20 request.httpMethod = "POST"21 request.setValue("Bearer (token)", forHTTPHeaderField: "Authorization")22 request.setValue("application/json", forHTTPHeaderField: "Content-Type")23 request.httpBody = try JSONSerialization.data(withJSONObject: body)24 25 let (data, _) = try await URLSession.shared.data(for: request)26 let response = try JSONDecoder().decode(UploadReservationResponse.self, from: data)27 return response.data.attributes.uploadUrl28 }29 30 // 2. TUS chunk upload31 func uploadChunk(32 uploadUrl: URL,33 data: Data,34 offset: Int,35 totalSize: Int36 ) async throws {37 var request = URLRequest(url: uploadUrl)38 request.httpMethod = "PATCH"39 request.setValue("application/offset+octet-stream", forHTTPHeaderField: "Content-Type")40 request.setValue("1.0.0", forHTTPHeaderField: "Tus-Resumable")41 request.setValue("(offset)", forHTTPHeaderField: "Upload-Offset")42 request.httpBody = data43 44 let (_, response) = try await URLSession.shared.data(for: request)45 guard let http = response as? HTTPURLResponse, http.statusCode == 204 else {46 throw AscError.uploadFailed47 }48 }49}Pratikte: xcodebuild + xcrun altcool Kombinasyonu
Gerçek production pipeline'ında çoğu ekip hâlâ şu kombinasyonu kullanıyor:
bash
1# Archive2xcodebuild archive -scheme MyApp -destination "generic/platform=iOS" -archivePath /tmp/MyApp.xcarchive CODE_SIGN_IDENTITY="Apple Distribution" PROVISIONING_PROFILE_SPECIFIER="MyApp Distribution"3 4# Export IPA5xcodebuild -exportArchive -archivePath /tmp/MyApp.xcarchive -exportPath /tmp/MyApp-export -exportOptionsPlist ExportOptions.plist6 7# Upload via xcrun notarytool (macOS 12+ replacement for altool)8xcrun notarytool submit /tmp/MyApp-export/MyApp.ipa --key /path/to/AuthKey.p8 --key-id YOUR_KEY_ID --issuer YOUR_ISSUER_ID --waitTestFlight Otomasyonu
Build'i TestFlight'a Hazırlama
swift
1extension AscClient {2 // Build'in TestFlight durumunu güncelle3 func updateBuildTestFlightStatus(4 buildId: String,5 testFlightDetails: TestFlightDetails6 ) async throws {7 let token = try await tokenCache.validToken(generator: generator)8 let body: [String: Any] = [9 "data": [10 "type": "betaBuildLocalizations",11 "attributes": [12 "whatsNew": testFlightDetails.whatsNew,13 "locale": testFlightDetails.locale14 ],15 "relationships": [16 "build": [17 "data": ["type": "builds", "id": buildId]18 ]19 ]20 ]21 ]22 23 var request = URLRequest(24 url: baseURL.appendingPathComponent("betaBuildLocalizations")25 )26 request.httpMethod = "POST"27 request.setValue("Bearer (token)", forHTTPHeaderField: "Authorization")28 request.setValue("application/json", forHTTPHeaderField: "Content-Type")29 request.httpBody = try JSONSerialization.data(withJSONObject: body)30 31 let (_, response) = try await URLSession.shared.data(for: request)32 guard let http = response as? HTTPURLResponse,33 http.statusCode == 201 else {34 throw AscError.testFlightUpdateFailed35 }36 }37 38 // External test grubuna build gönder (Apple review gerektirmez artık bazı durumlarda)39 func submitBuildForExternalTesting(buildId: String) async throws {40 let token = try await tokenCache.validToken(generator: generator)41 let body: [String: Any] = [42 "data": [43 "type": "betaAppReviewSubmissions",44 "relationships": [45 "build": [46 "data": ["type": "builds", "id": buildId]47 ]48 ]49 ]50 ]51 52 var request = URLRequest(53 url: baseURL.appendingPathComponent("betaAppReviewSubmissions")54 )55 request.httpMethod = "POST"56 request.setValue("Bearer (token)", forHTTPHeaderField: "Authorization")57 request.setValue("application/json", forHTTPHeaderField: "Content-Type")58 request.httpBody = try JSONSerialization.data(withJSONObject: body)59 60 let (_, response) = try await URLSession.shared.data(for: request)61 guard let http = response as? HTTPURLResponse,62 http.statusCode == 201 else {63 throw AscError.reviewSubmissionFailed64 }65 }66}67 68struct TestFlightDetails {69 let whatsNew: String70 let locale: String // örn. "tr"71}Otomatik Build Dağıtım Scripti
swift
1// CI/CD pipeline'ında çalışacak script2@main3struct TestFlightDeployScript {4 static func main() async {5 let generator = AscJwtGenerator(6 issuerId: ProcessInfo.processInfo.environment["ASC_ISSUER_ID"]!,7 keyId: ProcessInfo.processInfo.environment["ASC_KEY_ID"]!,8 privateKeyPem: ProcessInfo.processInfo.environment["ASC_PRIVATE_KEY"]!9 )10 let tokenCache = TokenCache()11 let client = AscClient(tokenCache: tokenCache, generator: generator)12 13 do {14 // 1. App bul15 let apps = try await client.fetchApps()16 guard let app = apps.first(where: { $0.attributes.bundleId == "com.example.myapp" }) else {17 print("App bulunamadi")18 exit(1)19 }20 21 // 2. Son build'i al22 let builds = try await client.fetchBuilds(appId: app.id, limit: 1)23 guard let latestBuild = builds.first else {24 print("Build bulunamadi")25 exit(1)26 }27 28 // 3. TestFlight notunu ekle29 let buildNumber = ProcessInfo.processInfo.environment["BUILD_NUMBER"] ?? "bilinmiyor"30 try await client.updateBuildTestFlightStatus(31 buildId: latestBuild.id,32 testFlightDetails: TestFlightDetails(33 whatsNew: "Build #(buildNumber) — Otomatik CI/CD deploy. Changelog: (ProcessInfo.processInfo.environment["CHANGELOG"] ?? "Yok")",34 locale: "tr"35 )36 )37 38 // 4. Internal test grubuna assign et39 let internalGroupId = ProcessInfo.processInfo.environment["TF_INTERNAL_GROUP_ID"]!40 try await client.addBuildToBetaGroup(buildId: latestBuild.id, groupId: internalGroupId)41 42 print("TestFlight deploy tamamlandi: Build (latestBuild.id)")43 } catch {44 print("Hata: (error)")45 exit(1)46 }47 }48}Metadata ve Submission Automation
App Store Version Metadata Update
swift
1extension AscClient {2 func updateAppStoreVersionLocalization(3 localizationId: String,4 description: String,5 keywords: String,6 promotionalText: String,7 whatsNew: String8 ) async throws {9 let token = try await tokenCache.validToken(generator: generator)10 let body: [String: Any] = [11 "data": [12 "type": "appStoreVersionLocalizations",13 "id": localizationId,14 "attributes": [15 "description": description,16 "keywords": keywords,17 "promotionalText": promotionalText,18 "whatsNew": whatsNew19 ]20 ]21 ]22 23 let url = baseURL24 .appendingPathComponent("appStoreVersionLocalizations")25 .appendingPathComponent(localizationId)26 var request = URLRequest(url: url)27 request.httpMethod = "PATCH"28 request.setValue("Bearer (token)", forHTTPHeaderField: "Authorization")29 request.setValue("application/json", forHTTPHeaderField: "Content-Type")30 request.httpBody = try JSONSerialization.data(withJSONObject: body)31 32 let (_, response) = try await URLSession.shared.data(for: request)33 guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {34 throw AscError.metadataUpdateFailed35 }36 }37 38 // App Review'a submit et39 func submitForReview(appStoreVersionId: String) async throws {40 let token = try await tokenCache.validToken(generator: generator)41 let body: [String: Any] = [42 "data": [43 "type": "appStoreVersionSubmissions",44 "relationships": [45 "appStoreVersion": [46 "data": ["type": "appStoreVersions", "id": appStoreVersionId]47 ]48 ]49 ]50 ]51 52 var request = URLRequest(53 url: baseURL.appendingPathComponent("appStoreVersionSubmissions")54 )55 request.httpMethod = "POST"56 request.setValue("Bearer (token)", forHTTPHeaderField: "Authorization")57 request.setValue("application/json", forHTTPHeaderField: "Content-Type")58 request.httpBody = try JSONSerialization.data(withJSONObject: body)59 60 let (_, response) = try await URLSession.shared.data(for: request)61 guard let http = response as? HTTPURLResponse, http.statusCode == 201 else {62 throw AscError.reviewSubmissionFailed63 }64 }65}App Store Connect GraphQL v2 (2025 Preview)
Apple 2025'te ASC GraphQL v2'yi preview olarak açtı. REST'e göre avantajı: tek request'te ihtiyaç duyduğun tüm alanları çekebiliyorsun.
graphql
1# Tek query ile app + versions + builds2query AppOverview($bundleId: String!) {3 app(bundleIdentifier: $bundleId) {4 id5 name6 currentVersionReleaseDate7 latestAppStoreVersion {8 versionString9 appStoreState10 localizations(languages: ["tr", "en-US"]) {11 locale12 description13 keywords14 whatsNew15 }16 }17 builds(first: 5) {18 edges {19 node {20 version21 processingState22 uploadedDate23 betaGroups {24 name25 publicLink26 }27 }28 }29 }30 }31}GraphQL endpoint henüz production'a çıkmadı (Q2 2026 bekleniyor), ama REST API'nin kısıtlamalarından kaçınmak için takip etmeye değer.
In-App Purchase ve Subscription Yönetimi
IAP Oluşturma
swift
1extension AscClient {2 func createInAppPurchase(3 appId: String,4 productId: String,5 name: String,6 purchaseType: IapType,7 price: IapPrice8 ) async throws -> String {9 let token = try await tokenCache.validToken(generator: generator)10 let body: [String: Any] = [11 "data": [12 "type": "inAppPurchases",13 "attributes": [14 "referenceName": name,15 "productId": productId,16 "inAppPurchaseType": purchaseType.rawValue17 ],18 "relationships": [19 "app": [20 "data": ["type": "apps", "id": appId]21 ]22 ]23 ]24 ]25 26 var request = URLRequest(27 url: baseURL.appendingPathComponent("inAppPurchasesV2")28 )29 request.httpMethod = "POST"30 request.setValue("Bearer (token)", forHTTPHeaderField: "Authorization")31 request.setValue("application/json", forHTTPHeaderField: "Content-Type")32 request.httpBody = try JSONSerialization.data(withJSONObject: body)33 34 let (data, response) = try await URLSession.shared.data(for: request)35 guard let http = response as? HTTPURLResponse, http.statusCode == 201 else {36 throw AscError.iapCreationFailed37 }38 let decoded = try JSONDecoder().decode(IapCreateResponse.self, from: data)39 return decoded.data.id40 }41 42 // Subscription offer code oluştur43 func createOfferCode(44 subscriptionId: String,45 name: String,46 customerEligibilityPaidSubscriptions: Bool,47 duration: String // örn. "ONE_MONTH"48 ) async throws -> String {49 let token = try await tokenCache.validToken(generator: generator)50 let body: [String: Any] = [51 "data": [52 "type": "subscriptionOfferCodes",53 "attributes": [54 "name": name,55 "customerEligibilityPaidSubscriptions": customerEligibilityPaidSubscriptions,56 "customerEligibilityIntroductoryOfferIneligible": false,57 "customerEligibilitySubscriptionCodeRedemptionIneligible": false,58 "offerEligibility": "NEW_SUBSCRIBERS",59 "priority": "NORMAL",60 "totalNumberOfCodes": 10061 ],62 "relationships": [63 "subscription": [64 "data": ["type": "subscriptions", "id": subscriptionId]65 ],66 "oneTimeUseCodes": [67 "data": [68 [69 "type": "subscriptionOfferCodeOneTimeUseCodes",70 "id": "$new"71 ]72 ]73 ]74 ]75 ]76 ]77 78 var request = URLRequest(79 url: baseURL.appendingPathComponent("subscriptionOfferCodes")80 )81 request.httpMethod = "POST"82 request.setValue("Bearer (token)", forHTTPHeaderField: "Authorization")83 request.setValue("application/json", forHTTPHeaderField: "Content-Type")84 request.httpBody = try JSONSerialization.data(withJSONObject: body)85 86 let (data, _) = try await URLSession.shared.data(for: request)87 let decoded = try JSONDecoder().decode(OfferCodeResponse.self, from: data)88 return decoded.data.id89 }90}91 92enum IapType: String {93 case consumable = "CONSUMABLE"94 case nonConsumable = "NON_CONSUMABLE"95 case nonRenewing = "NON_RENEWING_SUBSCRIPTION"96 case autoRenewing = "AUTO_RENEWABLE_SUBSCRIPTION"97}98 99struct IapPrice {100 let territory: String // örn. "TUR"101 let customerPrice: String // örn. "299.99"102}Analytics ve Sales Reports
Sales/Trends Report İndirme
ASC API üzerinden sales raporlarını programatik olarak çekebilirsin. Raporlar gzip sıkıştırılmış TSV formatında gelir.
swift
1struct AscReportsClient {2 let ascClient: AscClient3 4 enum ReportType: String {5 case sales = "SALES"6 case newsstand = "NEWSSTAND"7 case subscriptions = "SUBSCRIPTIONS"8 case subscriptionEvents = "SUBSCRIPTION_EVENTS"9 case subscribers = "SUBSCRIBERS"10 case preOrders = "PRE_ORDERS"11 }12 13 func downloadSalesReport(14 vendorId: String,15 reportType: ReportType,16 date: String, // "YYYY-MM-DD"17 frequency: String = "DAILY" // DAILY, WEEKLY, MONTHLY, YEARLY18 ) async throws -> [[String: String]] {19 let token = try await ascClient.tokenCache.validToken(generator: ascClient.generator)20 21 var components = URLComponents(22 url: ascClient.baseURL.appendingPathComponent("salesReports"),23 resolvingAgainstBaseURL: false24 )!25 components.queryItems = [26 URLQueryItem(name: "filter[reportType]", value: reportType.rawValue),27 URLQueryItem(name: "filter[reportSubType]", value: "SUMMARY"),28 URLQueryItem(name: "filter[frequency]", value: frequency),29 URLQueryItem(name: "filter[reportDate]", value: date),30 URLQueryItem(name: "filter[vendorNumber]", value: vendorId)31 ]32 33 var request = URLRequest(url: components.url!)34 request.setValue("Bearer (token)", forHTTPHeaderField: "Authorization")35 request.setValue("application/a-gzip", forHTTPHeaderField: "Accept")36 37 let (data, response) = try await URLSession.shared.data(for: request)38 guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {39 throw AscError.reportDownloadFailed40 }41 42 // Decompress gzip43 let decompressed = try (data as NSData).decompressed(using: .zlib) as Data44 let tsv = String(data: decompressed, encoding: .utf8) ?? ""45 46 // Parse TSV47 let lines = tsv.components(separatedBy: "48").filter { !$0.isEmpty }49 guard let headers = lines.first?.components(separatedBy: " ") else { return [] }50 51 return lines.dropFirst().map { line in52 let values = line.components(separatedBy: " ")53 return Dictionary(uniqueKeysWithValues: zip(headers, values))54 }55 }56 57 // App-specific metrics (installs, sessions, crashes, retention)58 func downloadAnalyticsReport(59 appId: String,60 metricType: String = "INSTALLS", // INSTALLS, SESSIONS, ACTIVE_DEVICES, CRASHES, PAYING_USERS61 startDate: String,62 endDate: String,63 granularity: String = "DAY"64 ) async throws -> Data {65 let token = try await ascClient.tokenCache.validToken(generator: ascClient.generator)66 67 var components = URLComponents(68 url: ascClient.baseURL.appendingPathComponent("analyticsReportRequests"),69 resolvingAgainstBaseURL: false70 )!71 72 // Önce report request oluştur73 let requestBody: [String: Any] = [74 "data": [75 "type": "analyticsReportRequests",76 "attributes": [77 "accessType": "ONGOING"78 ],79 "relationships": [80 "app": [81 "data": ["type": "apps", "id": appId]82 ]83 ]84 ]85 ]86 87 var request = URLRequest(url: components.url!)88 request.httpMethod = "POST"89 request.setValue("Bearer (token)", forHTTPHeaderField: "Authorization")90 request.setValue("application/json", forHTTPHeaderField: "Content-Type")91 request.httpBody = try JSONSerialization.data(withJSONObject: requestBody)92 93 let (data, _) = try await URLSession.shared.data(for: request)94 return data95 }96}Fastlane Spaceship Karşılaştırması
Hangi Yaklaşım Ne Zaman?
Senaryo | Öneri | Neden |
|---|---|---|
Yeni CI/CD pipeline | ASC API direkt | Dependency yok, Apple destekli |
Mevcut Fastlane pipeline | Fastlane devam | Migration riski |
Screenshot automation | Fastlane snapshot | ASC API screenshot yüklemeyi desteklemiyor |
Metadata update | ASC API | Fastlane deliver artık ASC API kullanıyor |
Signing & provisioning | Fastlane match | ASC API signing yönetimi sınırlı |
Build upload | xcrun notarytool | altool deprecated |
TestFlight tester yönetimi | ASC API | Fastlane pilot artık ASC API üzerinde |
Fastlane Lane Migration
ruby
1# Eski Fastlane lane2lane :beta do3 build_app(scheme: "MyApp")4 upload_to_testflight(5 api_key_path: "fastlane/api_key.json",6 skip_waiting_for_build_processing: false,7 distribute_external: true,8 groups: ["Beta Testers"],9 changelog: ENV["CHANGELOG"]10 )11end12 13# Yeni ASC API direkt yaklaşım (Swift script)14lane :beta_native do15 build_app(scheme: "MyApp")16 # xcrun ile upload17 sh("xcrun notarytool submit MyApp.ipa --key fastlane/AuthKey.p8 --key-id #{ENV['ASC_KEY_ID']} --issuer #{ENV['ASC_ISSUER_ID']} --wait")18 # ASC API ile TestFlight config19 sh("swift run TestFlightDeploy --build-number #{ENV['BUILD_NUMBER']} --changelog '#{ENV['CHANGELOG']}'")20endCI/CD Integration Patterns
GitHub Actions ile Tam Pipeline
yaml
1# .github/workflows/testflight.yml2name: TestFlight Deploy3 4on:5 push:6 branches: [main]7 workflow_dispatch:8 inputs:9 changelog:10 description: "Release notes"11 required: true12 13jobs:14 deploy:15 runs-on: macos-1416 steps:17 - uses: actions/checkout@v418 19 - name: Select Xcode 1620 run: sudo xcode-select -s /Applications/Xcode_16.app21 22 - name: Import signing certificate23 env:24 CERTIFICATE_BASE64: ${{ secrets.DIST_CERTIFICATE_BASE64 }}25 CERTIFICATE_PASSWORD: ${{ secrets.DIST_CERTIFICATE_PASSWORD }}26 KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}27 run: |28 KEYCHAIN_PATH=$RUNNER_TEMP/build.keychain29 security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH30 security set-keychain-settings -lut 21600 $KEYCHAIN_PATH31 security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH32 echo "$CERTIFICATE_BASE64" | base64 --decode > /tmp/cert.p1233 security import /tmp/cert.p12 -k $KEYCHAIN_PATH -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign34 35 - name: Download provisioning profile36 env:37 PP_BASE64: ${{ secrets.PROVISIONING_PROFILE_BASE64 }}38 run: |39 mkdir -p ~/Library/MobileDevice/Provisioning Profiles40 echo "$PP_BASE64" | base64 --decode > ~/Library/MobileDevice/Provisioning Profiles/MyApp.mobileprovision41 42 - name: Archive43 run: |44 xcodebuild archive -scheme MyApp -destination "generic/platform=iOS" -archivePath /tmp/MyApp.xcarchive CODE_SIGN_IDENTITY="Apple Distribution" PROVISIONING_PROFILE_SPECIFIER="MyApp Distribution"45 46 - name: Export IPA47 run: |48 xcodebuild -exportArchive -archivePath /tmp/MyApp.xcarchive -exportPath /tmp/MyApp-export -exportOptionsPlist ExportOptions.plist49 50 - name: Upload to TestFlight51 env:52 ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}53 ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}54 ASC_PRIVATE_KEY: ${{ secrets.ASC_PRIVATE_KEY }}55 run: |56 echo "$ASC_PRIVATE_KEY" > /tmp/AuthKey.p857 xcrun notarytool submit /tmp/MyApp-export/MyApp.ipa --key /tmp/AuthKey.p8 --key-id $ASC_KEY_ID --issuer $ASC_ISSUER_ID --wait58 59 - name: Notify Slack60 if: success()61 uses: slackapi/slack-github-action@v162 with:63 payload: |64 {65 "text": "TestFlight build hazir! Build #${{ github.run_number }}",66 "attachments": [{"color": "good", "text": "${{ github.event.inputs.changelog }}"}]67 }68 env:69 SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}Rate Limits ve Error Handling
Rate Limit Politikası
ASC API'nin rate limit'i sıkı değil ama dikkat etmek gerekiyor:
- Per endpoint:: Dakikada ~60-100 istek (Apple dokümantasyonunda kesin değer yok)
- Burst limit:: 10 saniyede 20 istek
- Daily limit:: Pratik olarak sınırsız, ama 429 alabilirsin
Robust Error Handling
swift
1enum AscError: LocalizedError {2 case httpError(Int)3 case rateLimited(retryAfter: Int)4 case unauthorized5 case invalidResponse6 case testerCreationFailed7 case buildAssignmentFailed8 case uploadFailed9 case testFlightUpdateFailed10 case metadataUpdateFailed11 case reviewSubmissionFailed12 case iapCreationFailed13 case reportDownloadFailed14 15 var errorDescription: String? {16 switch self {17 case .httpError(let code): return "HTTP (code)"18 case .rateLimited(let retry): return "Rate limit. (retry) saniye bekle."19 case .unauthorized: return "JWT token gecersiz veya suresi dolmus."20 default: return String(describing: self)21 }22 }23}24 25// Exponential backoff ile retry26func withRetry<T>(27 maxAttempts: Int = 3,28 baseDelay: TimeInterval = 1.0,29 operation: () async throws -> T30) async throws -> T {31 var lastError: Error?32 for attempt in 0..<maxAttempts {33 do {34 return try await operation()35 } catch AscError.rateLimited(let retryAfter) {36 try await Task.sleep(nanoseconds: UInt64(retryAfter) * 1_000_000_000)37 } catch {38 lastError = error39 if attempt < maxAttempts - 1 {40 let delay = baseDelay * pow(2.0, Double(attempt))41 try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))42 }43 }44 }45 throw lastError ?? AscError.invalidResponse46}ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.
swift
1// Sandbox tester oluştur (IAP test için)2extension AscClient {3 func createSandboxTester(4 email: String, // özel format: [email protected]5 firstName: String,6 lastName: String,7 territory: String = "TUR"8 ) async throws {9 let token = try await tokenCache.validToken(generator: generator)10 let body: [String: Any] = [11 "data": [12 "type": "sandboxTesters",13 "attributes": [14 "firstName": firstName,15 "lastName": lastName,16 "email": email,17 "password": UUID().uuidString + "Aa1!",18 "confirmPassword": UUID().uuidString + "Aa1!",19 "secretQuestion": "Favorite app?",20 "secretAnswer": "Mine",21 "birthDate": "1990-01-01",22 "territory": territory,23 "interruptPurchases": false24 ]25 ]26 ]27 28 var request = URLRequest(29 url: baseURL.appendingPathComponent("sandboxTesters")30 )31 request.httpMethod = "POST"32 request.setValue("Bearer (token)", forHTTPHeaderField: "Authorization")33 request.setValue("application/json", forHTTPHeaderField: "Content-Type")34 request.httpBody = try JSONSerialization.data(withJSONObject: body)35 36 let (_, response) = try await URLSession.shared.data(for: request)37 guard let http = response as? HTTPURLResponse, http.statusCode == 201 else {38 throw AscError.httpError((response as? HTTPURLResponse)?.statusCode ?? 0)39 }40 }41}Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
swift
1// AvdLee SDK kullanımı2import AppStoreConnect3 4let config = try APIConfiguration(5 issuerID: "your-issuer-id",6 privateKeyID: "your-key-id",7 privateKey: "your-private-key"8)9let provider = APIProvider(configuration: config)10 11let request = APIEndpoint.v1.apps.get(parameters: .init())12let apps = try await provider.request(request)Okuyucu Ödülü
Sıfırdan yazmak yerine community-maintained Swift package kullanabilirsin: - **[AvdLee/appstoreconnect-swift-sdk](https://github.com/AvdLee/appstoreconnect-swift-sdk):** En popüler, aktif maintained, type-safe models - **Apple'ın openapi.json:** `api.appstoreconnect.apple.com/v1` OpenAPI spec mevcut — kendi client'ını generate edebilirsin
Sonuç
App Store Connect API, iOS CI/CD pipeline'ının merkezine oturmuş durumda. JWT token yönetimi biraz zahmetli ama bir kez doğru kurulunca: build upload, TestFlight dağıtım, metadata güncelleme, IAP yönetimi ve sales analytics tamamen otomatik. Fastlane spaceship'in sihirli görünen "App Store Connect ile konuş" kısmı artık direkt API üzerinden yapılabilir — aradaki abstraction kaldırılınca neler döndüğü daha net görünüyor ve debug edilmesi çok daha kolay.
Daha fazla okuma için: iOS CI/CD Pipeline Rehberi, StoreKit 2 Production Guide, iOS 19 Beta Yenilikler, Senior iOS Developer 2026.
Kaynaklar:
- App Store Connect API Documentation — Apple resmi dokümantasyon
- AvdLee/appstoreconnect-swift-sdk — Community Swift SDK
- TUS Protocol Specification — Resumable upload protokolü
- Fastlane Documentation — Fastlane araç seti
- xcrun notarytool Guide — Apple notarization

