iOS Universal Links Setup
Universal Links let your iOS app open when a user taps an OpenLynk link, bypassing Safari entirely. This guide walks through every step from dashboard configuration to on-device testing.
Prerequisites
Before you begin, make sure you have:
- An iOS app with a valid Bundle ID (e.g.
com.example.myapp) - Your Apple Developer Team ID (found in Apple Developer under Membership)
- Your app registered in the OpenLynk dashboard with iOS settings configured
Universal Links require HTTPS and a properly served Apple App Site Association (AASA) file. OpenLynk handles both of these for you automatically.
Step 1: Configure Your App in OpenLynk
In the OpenLynk dashboard, go to your app and open Settings. Fill in the iOS fields:
| Field | Example | Description |
|---|---|---|
| iOS Bundle ID | com.example.myapp | Must match your Xcode project exactly |
| iOS Team ID | A1B2C3D4E5 | Found in Apple Developer under Membership |
| iOS App Store URL | https://apps.apple.com/app/id123456789 | Fallback when the app is not installed |
Once saved, OpenLynk automatically generates and hosts the AASA file at:
https://YOUR_APP_SLUG.openlynk.to/.well-known/apple-app-site-association
You do not need to create, host, or maintain the AASA file yourself. OpenLynk keeps it in sync whenever you update your iOS settings.
Step 2: Add Associated Domains in Xcode
- Open your project in Xcode.
- Select your app target, then go to Signing & Capabilities.
- Click + Capability and add Associated Domains.
- Add your OpenLynk domain:
applinks:YOUR_APP_SLUG.openlynk.to
If you are using a custom domain, add that as well:
applinks:links.mycompany.com
The domain must match exactly, including case. Do not include https:// or a trailing slash.
Step 3: Handle Universal Links in Code
When a user taps a Universal Link, iOS delivers it to your app through SceneDelegate (iOS 13+) or AppDelegate. You must pass the incoming URL to the OpenLynk SDK so it can resolve the link and call your onDeepLink callback.
With SceneDelegate (iOS 13+)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var openlynkSDK: OpenlynkSDK!
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
guard let windowScene = scene as? UIWindowScene else { return }
// Initialize SDK
openlynkSDK = OpenlynkSDK(
appId: "YOUR_APP_ID",
apiKey: "YOUR_API_KEY",
config: OpenlynkSDKConfig(
onDeepLink: { [weak self] parsed in
self?.navigateTo(
path: parsed.destinationPath,
params: parsed.parameters
)
}
)
)
openlynkSDK.initSDK()
// Handle Universal Link from cold start
if let userActivity = connectionOptions.userActivities.first,
userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL {
openlynkSDK.handleIncomingURL(url)
}
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UINavigationController(
rootViewController: HomeViewController()
)
self.window = window
window.makeKeyAndVisible()
}
// Universal Link — app already running in background
func scene(_ scene: UIScene,
continue userActivity: NSUserActivity) {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else { return }
openlynkSDK.handleIncomingURL(url)
}
// URL scheme or other URL contexts
func scene(_ scene: UIScene,
openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let url = URLContexts.first?.url else { return }
openlynkSDK.handleIncomingURL(url)
}
private func navigateTo(path: String, params: [String: Any]) {
// Your navigation logic
}
}
With AppDelegate (Pre-iOS 13)
If your app does not use SceneDelegate:
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var openlynkSDK: OpenlynkSDK!
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
openlynkSDK = OpenlynkSDK(
appId: "YOUR_APP_ID",
apiKey: "YOUR_API_KEY",
config: OpenlynkSDKConfig(
onDeepLink: { parsed in /* navigate */ }
)
)
openlynkSDK.initSDK()
return true
}
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else { return false }
openlynkSDK.handleIncomingURL(url)
return true
}
}
Step 4: Verify the AASA File
OpenLynk generates the AASA file automatically. Verify it is accessible by visiting:
https://YOUR_APP_SLUG.openlynk.to/.well-known/apple-app-site-association
Confirm the following:
- The page loads with an HTTP 200 status
- The response is valid JSON
- The
appIDvalue matches{TeamID}.{BundleID}exactly (e.g.A1B2C3D4E5.com.example.myapp)
You can also use Apple's official validation tool: https://search.developer.apple.com/appleassociateddomainsverification/
Step 5: Test on a Physical Device
Universal Links do not work in the iOS Simulator. You must test on a real device.
Test Link Recognition
- Open the Notes app on your device.
- Type or paste:
https://YOUR_APP_SLUG.openlynk.to/test-link - Long-press the link.
- You should see "Open in [Your App]" in the context menu.
Test Link Opening
- Generate a real link via the SDK or the OpenLynk dashboard.
- Send it to yourself via Messages or Mail.
- Tap the link.
- Your app should open directly (not Safari).
- Verify your
onDeepLinkcallback fires with the correct destination.
Test from Safari
- Navigate to your link URL in Safari.
- If a banner appears at the top, tap "Open".
- Alternatively, use the Share Sheet and select your app.
Troubleshooting
Link opens in Safari instead of the app
| Cause | Fix |
|---|---|
| Associated Domains not configured | Add applinks:YOUR_APP_SLUG.openlynk.to in Xcode |
| Bundle ID mismatch | Ensure the Xcode Bundle ID matches the dashboard value exactly |
| AASA file inaccessible | Visit the AASA URL in a browser and confirm a valid JSON response |
| Stale AASA cache | Delete and reinstall the app. iOS caches AASA for up to 24 hours |
| In-app browser | Universal Links do not work inside Instagram, Facebook, or Twitter browsers |
"Open in App" option does not appear
- Delete the app and reinstall it.
- Wait a few minutes after install (iOS fetches the AASA file asynchronously).
- Ensure the link is typed or pasted, not auto-linked text.
- Try from the Notes or Messages app (not the Safari URL bar).
App opens but does not navigate
- Verify your
handleIncomingURLimplementation is being called. - Add logging to confirm the URL is passed to the SDK.
- Check that the link exists in the OpenLynk dashboard.
Works in development, fails in production
- Ensure the production build has the Associated Domains capability enabled.
- Verify the production AASA file references the correct Bundle ID.
- Confirm the provisioning profile includes the Associated Domains entitlement.
Checklist
- Bundle ID in Xcode matches the OpenLynk dashboard
- Team ID entered in the OpenLynk dashboard
- Associated Domains capability added in Xcode
-
applinks:YOUR_APP_SLUG.openlynk.toadded to associated domains - AASA file accessible (HTTP 200, valid JSON)
-
appIDin AASA matches{TeamID}.{BundleID} -
handleIncomingURLcalled in SceneDelegate or AppDelegate - Tested on a physical device (not the simulator)
- Link shows "Open in App" in Notes
- Link opens app directly from Messages
- Navigation works from cold start, background, and foreground
What's Next?
- Learn more about how deep linking works under the hood.
- See the full iOS SDK reference for all available methods and configuration options.
- Set up Android App Links if your app also targets Android.