iOS SDK Integration (RevenueCat)
Track affiliate-driven iOS app installs and in-app purchases with Universal Links and RevenueCat webhooks.
Prerequisites
- iOS app with RevenueCat for in-app purchases
- Xcode project with Associated Domains capability
- RevenueCat project configured
- Apple Developer Program membership
Mobile app attribution is more complex than web tracking due to the App Store acting as an intermediary. This guide covers the complete iOS integration, including Universal Links for direct attribution, deferred attribution for App Store installs, and RevenueCat webhook configuration.
Attribution Methods
- Universal Links (Direct) - User clicks link, app opens directly, attribution is instant
- Deferred Attribution - User clicks link, goes to App Store, installs, then attribution is matched on first launch
- RevenueCat Webhooks - In-app purchase events sent to Attro for conversion tracking
Privacy Note: iOS 14.5+ requires ATT consent for IDFA tracking. Attro uses privacy-friendly fingerprinting for deferred attribution that doesn't require IDFA.
Configure Universal Links
Universal Links allow your app to open directly when users tap Attro tracking links, providing instant attribution.
Add Associated Domains in Xcode
- Open your project in Xcode
- Select your target → Signing & Capabilities
- Click "+ Capability" and add "Associated Domains"
- Add the Attro domain
// Add this associated domain:
applinks:get-attro.comHandle Incoming URLs
In your SceneDelegate.swift (or AppDelegate for older apps):
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
// Handle Universal Links when app is already running
func scene(_ scene: UIScene,
continue userActivity: NSUserActivity) {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else {
return
}
handleAttroLink(url)
}
// Handle Universal Links on cold start
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
// Check for Universal Link in connection options
if let userActivity = connectionOptions.userActivities.first,
userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL {
handleAttroLink(url)
}
}
private func handleAttroLink(_ url: URL) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
return
}
// Parse query parameters
var params: [String: String] = [:]
components.queryItems?.forEach { item in
if let value = item.value {
params[item.name] = value
}
}
// Extract attribution data
if let affiliateId = params["affiliate_id"],
let offerId = params["offer_id"],
let clickId = params["click_id"] {
// Set RevenueCat attributes
setRevenueCatAttribution(
clickId: clickId,
affiliateId: affiliateId,
offerId: offerId
)
}
}
}Testing Tip: Test Universal Links from Notes or Messages app (not Safari address bar). Clicking from Safari may not trigger Universal Links.
Set RevenueCat Subscriber Attributes
Store attribution data as RevenueCat subscriber attributes. When a purchase occurs, these attributes are included in webhook events sent to Attro.
Attribution Function
import RevenueCat
class AttributionManager {
static let shared = AttributionManager()
/// Store attribution data in RevenueCat subscriber attributes
func setAttribution(clickId: String, affiliateId: String, offerId: String) {
Purchases.shared.attribution.setAttributes([
"$rd_click_id": clickId,
"$rd_affiliate_id": affiliateId,
"$rd_offer_id": offerId,
"$rd_attributed_at": ISO8601DateFormatter().string(from: Date())
])
// Also store locally for reference
UserDefaults.standard.set(clickId, forKey: "attro_click_id")
UserDefaults.standard.set(affiliateId, forKey: "attro_affiliate_id")
UserDefaults.standard.set(offerId, forKey: "attro_offer_id")
print("Attro attribution set: click=\(clickId), affiliate=\(affiliateId)")
}
/// Check if user has attribution
var hasAttribution: Bool {
return UserDefaults.standard.string(forKey: "attro_click_id") != nil
}
}
// Usage from SceneDelegate:
func setRevenueCatAttribution(clickId: String, affiliateId: String, offerId: String) {
AttributionManager.shared.setAttribution(
clickId: clickId,
affiliateId: affiliateId,
offerId: offerId
)
}Attribute Names
RevenueCat attributes prefixed with $ are reserved and included in webhook payloads:
$rd_click_id- Unique click identifier$rd_affiliate_id- Affiliate who referred$rd_offer_id- Offer being promoted$rd_attributed_at- Timestamp of attribution
Implement Deferred Attribution
Deferred attribution handles the case where a user clicks an affiliate link, then goes to the App Store to install your app. On first launch, you'll query Attro to check for a matching click.
How It Works
- User clicks affiliate tracking link
- Attro records click with device fingerprint (screen size, timezone, language)
- User redirected to App Store, installs app
- On first launch, app sends fingerprint to Attro
- Attro matches fingerprint to recent click
- Attribution data returned and stored
Implementation
import UIKit
class DeferredAttributionManager {
static let shared = DeferredAttributionManager()
private let attributionCheckedKey = "attro_deferred_attribution_checked"
/// Check for deferred attribution on first launch
func checkDeferredAttribution() async {
// Only check once
guard !UserDefaults.standard.bool(forKey: attributionCheckedKey) else {
return
}
// Skip if already attributed via Universal Link
guard !AttributionManager.shared.hasAttribution else {
UserDefaults.standard.set(true, forKey: attributionCheckedKey)
return
}
do {
let attribution = try await fetchDeferredAttribution()
if let attr = attribution {
AttributionManager.shared.setAttribution(
clickId: attr.clickId,
affiliateId: attr.affiliateId,
offerId: attr.offerId
)
print("Deferred attribution matched!")
} else {
print("No deferred attribution found")
}
UserDefaults.standard.set(true, forKey: attributionCheckedKey)
} catch {
print("Deferred attribution check failed: \(error)")
// Don't mark as checked - retry on next launch
}
}
private func fetchDeferredAttribution() async throws -> AttributionData? {
let url = URL(string: "https://get-attro.com/api/ios/match")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// Collect device fingerprint
let screen = await UIScreen.main.bounds
let scale = await UIScreen.main.scale
let body: [String: Any] = [
"screenWidth": Int(screen.width * scale),
"screenHeight": Int(screen.height * scale),
"timezone": TimeZone.current.identifier,
"language": Locale.current.language.languageCode?.identifier ?? "en",
"model": await UIDevice.current.model,
"systemVersion": await UIDevice.current.systemVersion
]
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
return nil
}
let result = try JSONDecoder().decode(MatchResponse.self, from: data)
return result.matched ? result.attribution : nil
}
}
struct MatchResponse: Codable {
let matched: Bool
let attribution: AttributionData?
}
struct AttributionData: Codable {
let clickId: String
let affiliateId: String
let offerId: String
enum CodingKeys: String, CodingKey {
case clickId = "click_id"
case affiliateId = "affiliate_id"
case offerId = "offer_id"
}
}Call on App Launch
// In your App struct or AppDelegate
@main
struct YourApp: App {
init() {
// Configure RevenueCat first
Purchases.configure(withAPIKey: "your_revenuecat_api_key")
// Check for deferred attribution
Task {
await DeferredAttributionManager.shared.checkDeferredAttribution()
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}Match Window: Deferred attribution matches within 24 hours by default. This balances accuracy with privacy.
Configure RevenueCat Webhooks
RevenueCat webhooks notify Attro when users make in-app purchases. This is how conversions are tracked.
Set Up in RevenueCat Dashboard
- Log into RevenueCat Dashboard
- Go to your project → Integrations
- Click "Webhooks"
- Click "Add Webhook"
Webhook Configuration
Webhook URL:
https://get-attro.com/api/webhooks/revenuecatEvents to subscribe:
INITIAL_PURCHASE- New subscription or one-time purchaseRENEWAL- Subscription renewedNON_RENEWING_PURCHASE- Consumable or non-renewing purchaseCANCELLATION- (Optional) Track cancellations
Authorization Header
Add an authorization header for security:
- In Attro, go to Settings → Integrations → RevenueCat
- Copy the webhook auth key
- In RevenueCat webhook settings, add header:
Header Name: Authorization
Header Value: Bearer YOUR_AUTH_KEYTest the Webhook
RevenueCat provides a "Send Test" button to verify connectivity:
- Click "Send Test" in RevenueCat
- Check Attro logs for received event
- Verify the event was processed successfully
Implement Refer-a-Friend (Optional)
Allow existing users to earn tokens by referring friends. This creates a powerful growth loop.
Fetch User's Referral Link
import RevenueCat
class ReferralManager {
static let shared = ReferralManager()
/// Fetch or create referral link for current user
func getReferralLink() async throws -> ReferralData {
// Get RevenueCat user ID
let customerInfo = try await Purchases.shared.customerInfo()
let userId = customerInfo.originalAppUserId
let url = URL(string: "https://get-attro.com/api/ios/my-affiliate")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: String] = [
"revenuecat_user_id": userId
]
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, _) = try await URLSession.shared.data(for: request)
return try JSONDecoder().decode(ReferralData.self, from: data)
}
/// Fetch referral stats
func getReferralStats() async throws -> ReferralStats {
let customerInfo = try await Purchases.shared.customerInfo()
let userId = customerInfo.originalAppUserId
let url = URL(string: "https://get-attro.com/api/ios/my-affiliate/stats?user_id=\(userId)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(ReferralStats.self, from: data)
}
}
struct ReferralData: Codable {
let trackingUrl: String
let code: String
let tokensEarned: Int
enum CodingKeys: String, CodingKey {
case trackingUrl = "tracking_url"
case code
case tokensEarned = "tokens_earned"
}
}
struct ReferralStats: Codable {
let totalReferrals: Int
let tokensEarned: Int
let pendingTokens: Int
enum CodingKeys: String, CodingKey {
case totalReferrals = "total_referrals"
case tokensEarned = "tokens_earned"
case pendingTokens = "pending_tokens"
}
}Share Sheet Integration
import SwiftUI
struct ReferralView: View {
@State private var referralData: ReferralData?
@State private var isLoading = true
@State private var showShareSheet = false
var body: some View {
VStack(spacing: 20) {
if isLoading {
ProgressView()
} else if let data = referralData {
Text("Earn tokens for every friend who subscribes!")
.font(.headline)
Text("Your referral code: \(data.code)")
.font(.title2)
.bold()
Text("Tokens earned: \(data.tokensEarned)")
.foregroundColor(.secondary)
Button("Share Referral Link") {
showShareSheet = true
}
.buttonStyle(.borderedProminent)
}
}
.sheet(isPresented: $showShareSheet) {
if let url = referralData?.trackingUrl {
ShareSheet(items: [
"Check out this app! \(url)"
])
}
}
.task {
do {
referralData = try await ReferralManager.shared.getReferralLink()
} catch {
print("Failed to load referral: \(error)")
}
isLoading = false
}
}
}
struct ShareSheet: UIViewControllerRepresentable {
let items: [Any]
func makeUIViewController(context: Context) -> UIActivityViewController {
UIActivityViewController(activityItems: items, applicationActivities: nil)
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
}Test Your Integration
Thoroughly test each attribution path to ensure conversions are tracked correctly.
Test Universal Links
- Create a test tracking link in Attro admin
- Copy the link
- On your test device, paste into Notes app
- Tap the link - your app should open directly
- Check debug logs for attribution data
- Make a sandbox purchase
- Verify conversion in Attro dashboard
Test Deferred Attribution
- Delete your app from the test device
- Click a tracking link in Safari
- When redirected to App Store, install via TestFlight instead
- Open the app
- Check logs for deferred attribution match
- Make a sandbox purchase
- Verify conversion in Attro dashboard
Sandbox Purchases
Use App Store sandbox for testing:
- Create sandbox tester in App Store Connect
- Sign out of App Store on device
- Make purchase - will prompt for sandbox credentials
- Sandbox purchases trigger real RevenueCat webhooks
Debug Logging
// Enable verbose logging during development
#if DEBUG
Purchases.logLevel = .verbose
#endif
// Add attribution logging
func setAttribution(clickId: String, affiliateId: String, offerId: String) {
#if DEBUG
print("""
[Attro] Setting attribution:
Click ID: \(clickId)
Affiliate: \(affiliateId)
Offer: \(offerId)
""")
#endif
// ... rest of implementation
}RevenueCat Dashboard: Check Customer → Select User → Attributes to verify attribution data is being stored correctly.
Integration Complete
Your iOS app is now fully integrated with Attro for affiliate tracking. Both direct (Universal Links) and deferred attribution paths are covered, and RevenueCat webhooks will automatically track conversions.
Architecture Summary
- Universal Links provide instant attribution when app is installed
- Deferred attribution matches users who install from App Store
- RevenueCat subscriber attributes store attribution data
- Webhooks notify Attro of purchases for conversion tracking
Next Steps
- Commission Structures - Configure token rewards for internal affiliates
- Fraud Detection - Protect against fraudulent installs
- API Integration - Build custom referral experiences
Need help with your iOS integration? Contact us at [email protected].
Related guides
Zapier & Webhook Automations
Automate your affiliate workflow with Zapier or custom webhooks. Trigger actions on signups, conversions, payouts, and more.
White-Label Portal Setup
Give affiliates a branded experience with your own domain, logo, and colors. The white-label portal appears as part of your product.
Need help with integration?
Our support team is here to help you get set up.