Guides·Advanced

iOS SDK Integration (RevenueCat)

Track affiliate-driven iOS app installs and in-app purchases with Universal Links and RevenueCat webhooks.

11 minLast updated June 13, 2026

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.

1

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

  1. Open your project in Xcode
  2. Select your target → Signing & Capabilities
  3. Click "+ Capability" and add "Associated Domains"
  4. Add the Attro domain
// Add this associated domain:
applinks:get-attro.com

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

2

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
3

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

  1. User clicks affiliate tracking link
  2. Attro records click with device fingerprint (screen size, timezone, language)
  3. User redirected to App Store, installs app
  4. On first launch, app sends fingerprint to Attro
  5. Attro matches fingerprint to recent click
  6. 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.

4

Configure RevenueCat Webhooks

RevenueCat webhooks notify Attro when users make in-app purchases. This is how conversions are tracked.

Set Up in RevenueCat Dashboard

  1. Log into RevenueCat Dashboard
  2. Go to your project → Integrations
  3. Click "Webhooks"
  4. Click "Add Webhook"

Webhook Configuration

Webhook URL:

https://get-attro.com/api/webhooks/revenuecat

Events to subscribe:

  • INITIAL_PURCHASE - New subscription or one-time purchase
  • RENEWAL - Subscription renewed
  • NON_RENEWING_PURCHASE - Consumable or non-renewing purchase
  • CANCELLATION - (Optional) Track cancellations

Authorization Header

Add an authorization header for security:

  1. In Attro, go to Settings → Integrations → RevenueCat
  2. Copy the webhook auth key
  3. In RevenueCat webhook settings, add header:
Header Name: Authorization
Header Value: Bearer YOUR_AUTH_KEY

Test the Webhook

RevenueCat provides a "Send Test" button to verify connectivity:

  1. Click "Send Test" in RevenueCat
  2. Check Attro logs for received event
  3. Verify the event was processed successfully
5

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) {}
}
6

Test Your Integration

Thoroughly test each attribution path to ensure conversions are tracked correctly.

Test Universal Links

  1. Create a test tracking link in Attro admin
  2. Copy the link
  3. On your test device, paste into Notes app
  4. Tap the link - your app should open directly
  5. Check debug logs for attribution data
  6. Make a sandbox purchase
  7. Verify conversion in Attro dashboard

Test Deferred Attribution

  1. Delete your app from the test device
  2. Click a tracking link in Safari
  3. When redirected to App Store, install via TestFlight instead
  4. Open the app
  5. Check logs for deferred attribution match
  6. Make a sandbox purchase
  7. 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

  1. Universal Links provide instant attribution when app is installed
  2. Deferred attribution matches users who install from App Store
  3. RevenueCat subscriber attributes store attribution data
  4. Webhooks notify Attro of purchases for conversion tracking

Next Steps

Need help with your iOS integration? Contact us at [email protected].

Need help with integration?

Our support team is here to help you get set up.