Deferred deep linking on iOS is more constrained than on Android, primarily because Apple controls far more of the stack. The absence of a Play Install Referrer equivalent, the introduction of App Tracking Transparency, and the behavioral differences between SceneDelegate and AppDelegate all shape what is possible.
This guide walks through the complete iOS implementation: detecting a first open, retrieving deferred link data, routing users to the right screen, and handling the cases where attribution is unavailable.
How iOS Deferred Deep Linking Works
When a user taps a deep link on iOS before your app is installed, Safari or the embedding app opens the App Store. The link context (the URL, any campaign parameters, referral codes) cannot ride through the App Store to your app the way Android's Install Referrer does. iOS does not have a native mechanism for this.
Attribution platforms fill this gap through one of three techniques:
Probabilistic fingerprinting. The click captures device signals (IP, OS version, device model, etc.). On first open, your app sends the same signals to the attribution server, which attempts to match them to a recent click within a match window.
IDFA matching. If the user grants App Tracking Transparency permission, your app can read the IDFA and send it to the attribution server for an exact match against the IDFA captured at click time.
Clipboard (pasteboard) tokens. When the user taps the link, a short-lived token is written to the clipboard. On first open, your app reads the clipboard and sends the token for an exact match. As of iOS 16, reading the clipboard from an app triggers a system notification. As of iOS 17, it requires explicit user permission in some contexts.
Tolinku uses all three, in order of accuracy, with configurable fallbacks.
Prerequisites
- Xcode 15 or later
- iOS 15+ deployment target (recommended; iOS 14 minimum)
- Swift Package Manager or CocoaPods
- An active Tolinku Appspace with a publishable API key
Add the Tolinku iOS SDK via Swift Package Manager:
https://github.com/tolinku/ios-sdkOr via CocoaPods:
pod 'TolinkuSDK'The full SDK reference is at tolinku.com/docs/developer/sdks/ios/.
Detecting a First Open
The most important thing to get right: only run deferred link resolution on the true first open of the app. Running it on every launch creates unnecessary network requests and risks routing returning users to onboarding screens they have already seen.
Store a flag in
UserDefaultsthe first time the app completes attribution resolution:import Foundation struct InstallState { private static let key = "tolk_first_open_resolved" static var hasResolvedFirstOpen: Bool { get { UserDefaults.standard.bool(forKey: key) } set { UserDefaults.standard.set(newValue, forKey: key) } } }Check this flag before triggering any resolution logic.
SceneDelegate Integration

Apps using UISceneDelegate (the default for iOS 13+) should initialize Tolinku and run first-open logic in
scene(_:willConnectTo:options:):import UIKit import TolinkuSDK class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene( _ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions ) { guard let windowScene = scene as? UIWindowScene else { return } window = UIWindow(windowScene: windowScene) Tolinku.configure(publishableKey: "tolk_pub_your_key_here") if !InstallState.hasResolvedFirstOpen { resolveFirstOpen() } else { showMainInterface() } } private func resolveFirstOpen() { Tolinku.shared.resolveDeferred { result in DispatchQueue.main.async { InstallState.hasResolvedFirstOpen = true switch result { case .success(let deepLink): self.route(to: deepLink) case .failure: self.showMainInterface() } } } } private func route(to deepLink: TolinkuDeepLink) { // deepLink.path, deepLink.params, deepLink.referralCode, etc. // Route to the appropriate screen based on the link data showMainInterface(initialRoute: deepLink) } private func showMainInterface(initialRoute: TolinkuDeepLink? = nil) { let vc = MainViewController(deepLink: initialRoute) window?.rootViewController = UINavigationController(rootViewController: vc) window?.makeKeyAndVisible() } }The
resolveDeferredcall handles fingerprinting, IDFA lookup (if available), and clipboard reading internally. It returns either aTolinkuDeepLinkwith the routing data or an error if no matching click was found.Handling AppDelegate-Only Apps
If your app still uses
AppDelegatewithout scenes (less common but still valid), run resolution inapplication(_:didFinishLaunchingWithOptions:):func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { Tolinku.configure(publishableKey: "tolk_pub_your_key_here") if !InstallState.hasResolvedFirstOpen { Tolinku.shared.resolveDeferred { result in DispatchQueue.main.async { InstallState.hasResolvedFirstOpen = true // Handle routing } } } return true }ATT and IDFA Access
Requesting ATT permission is optional but increases attribution accuracy. If you choose to request it, do so at a moment that makes sense for your app's flow, not immediately on launch (Apple's guidelines recommend providing context before the system prompt appears).
Add
NSUserTrackingUsageDescriptionto yourInfo.plist:<key>NSUserTrackingUsageDescription</key> <string>We use this to attribute your install and personalize your experience.</string>Then request permission before calling
resolveDeferred:import AppTrackingTransparency import AdSupport func requestTrackingAndResolve() { if #available(iOS 14, *) { ATTrackingManager.requestTrackingAuthorization { status in // Proceed with resolution regardless of status. // Tolinku will use IDFA if granted, fingerprinting otherwise. Tolinku.shared.resolveDeferred { result in DispatchQueue.main.async { self.handleDeferredResult(result) } } } } else { Tolinku.shared.resolveDeferred { result in DispatchQueue.main.async { self.handleDeferredResult(result) } } } }Always call
resolveDeferredinside the ATT completion handler, not before it. If you call resolution before the ATT prompt resolves, the SDK cannot read the IDFA and will fall back to fingerprinting even if the user intends to grant permission.Clipboard / Pasteboard Matching
Tolinku's SDK handles clipboard matching internally when fingerprinting is the only available option. On iOS 16+, reading the clipboard from an app causes the system to display a brief notification at the top of the screen indicating that your app read from the clipboard. This is not a blocking permission prompt, but users do see it.
If this creates a bad experience for your app, you can disable pasteboard matching in the SDK configuration:
var config = TolinkuConfiguration(publishableKey: "tolk_pub_your_key_here") config.pasteboardMatchingEnabled = false Tolinku.configure(with: config)Disabling it means fingerprinting is the only fallback when ATT is not granted. Attribution accuracy will decrease, but the clipboard notification disappears.
SKAdNetwork Interaction
SKAdNetwork is Apple's privacy-preserving install attribution framework. It tells ad networks which campaigns drove installs, with aggregated and delayed reporting. It does not support deferred deep linking.
If you are running paid UA campaigns, register your SKAdNetwork IDs in
Info.plistper the Apple documentation. This is separate from your Tolinku integration. Tolinku handles the deep link routing; SKAdNetwork handles aggregated conversion reporting to ad networks.You can configure a
SKAdNetworkconversion value update in parallel with Tolinku's first-open resolution if you need to report on conversion values for paid channels.Testing Your Implementation
Testing deferred deep linking on iOS requires actual devices. The iOS Simulator does not replicate the App Store install flow.
A practical testing workflow:
- Generate a test link from your Tolinku dashboard.
- On a physical iOS device that does not have your app installed, tap the link. Confirm it redirects to the App Store.
- Install the app via TestFlight or a direct install (Xcode device run works for development).
- Open the app. Verify that
resolveDeferredis called and that the callback receives the expected deep link data. - Close and reopen the app. Verify that the deferred resolution does NOT run again (the
hasResolvedFirstOpenguard should prevent it).
If you have issues, enable verbose logging:
Tolinku.setLogLevel(.debug)This prints resolution steps to the console, showing which matching method was used and what data was returned.
Routing After Resolution
The
TolinkuDeepLinkobject contains:path: the original link path (e.g.,/referral/ABC123)params: a dictionary of URL parametersreferralCode: convenience accessor if a referral code is presentcampaignId,source,medium: attribution data if available
Use these to drive your routing logic. For complex apps with many possible destinations, a central routing coordinator or a
DeepLinkRouterclass handles this cleanly:class DeepLinkRouter { static func route(deepLink: TolinkuDeepLink, from nav: UINavigationController) { switch deepLink.path { case _ where deepLink.path.hasPrefix("/referral"): let vc = ReferralOnboardingViewController(code: deepLink.referralCode) nav.setViewControllers([vc], animated: false) case _ where deepLink.path.hasPrefix("/promo"): let vc = PromoViewController(params: deepLink.params) nav.setViewControllers([vc], animated: false) default: nav.setViewControllers([MainViewController()], animated: false) } } }For more on building personalized onboarding flows using deferred link data, see the deferred deep linking for onboarding guide.
Further Reading
Get deep linking tips in your inbox
One email per week. No spam.