{"id":364,"date":"2026-03-05T13:00:00","date_gmt":"2026-03-05T18:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=364"},"modified":"2026-03-07T04:33:11","modified_gmt":"2026-03-07T09:33:11","slug":"universal-links-everything-you-need-to-know","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/universal-links-everything-you-need-to-know\/","title":{"rendered":"Universal Links: Everything You Need to Know"},"content":{"rendered":"\n<p>Universal links let iOS users tap a standard HTTPS URL and land directly inside your app. No custom scheme. No browser redirect. No confirmation dialog asking &quot;Open in App?&quot; The link just works. If the app is installed, it opens. If not, the URL loads in Safari like any normal web page.<\/p>\n\n\n\n<p>Apple <a href=\"https:\/\/developer.apple.com\/ios\/universal-links\/\" rel=\"nofollow noopener\" target=\"_blank\">introduced universal links in iOS 9<\/a> to replace the fragile <code>yourapp:\/\/<\/code> custom URI scheme pattern. Custom schemes had real problems: they showed ugly error dialogs when the app was not installed, they could be hijacked by other apps claiming the same scheme, and they required awkward JavaScript hacks to detect whether the app was present. Universal links solved all of that by tying your app to your domain through a cryptographic verification process.<\/p>\n\n\n\n<p>If you are building an iOS app that needs to open from links (emails, social media, QR codes, websites, or marketing campaigns), universal links are the standard you should be using. This guide walks through how they work, how to set them up, and how to fix them when they break.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">How Universal Links Work<\/h2>\n\n\n\n<p>The mechanism behind universal links is straightforward, but there are several moving parts.<\/p>\n\n\n\n<p>Here is the full flow:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><p><strong>You host a file on your web server.<\/strong> This file is called <code>apple-app-site-association<\/code> (AASA). It lives at <code>https:\/\/yourdomain.com\/.well-known\/apple-app-site-association<\/code>. It tells Apple which URL paths your app can handle.<\/p>\n\n\n\n<p><strong>Apple downloads and caches the AASA file.<\/strong> When a user installs your app (or updates it), Apple&#39;s CDN fetches your AASA file and caches the result. This happens on Apple&#39;s servers, not on the user&#39;s device. Starting in iOS 14, Apple moved this verification to their own <a href=\"https:\/\/developer.apple.com\/documentation\/bundleresources\/applinks\" rel=\"nofollow noopener\" target=\"_blank\">CDN<\/a> instead of having each device fetch it directly.<\/p>\n\n\n\n<p><strong>A user taps a link.<\/strong> When someone taps a URL that matches a pattern in your AASA file, iOS checks its cached copy of the file. If there is a match and your app is installed, iOS opens the app and passes the URL to it.<\/p>\n\n\n\n<p><strong>Your app receives the URL.<\/strong> Your app gets the URL through the <code>NSUserActivity<\/code> API. You parse the URL, extract the path and parameters, and navigate to the correct screen.<\/p>\n\n\n\n<p>The key detail here: Apple&#39;s CDN fetches your AASA file, not the user&#39;s device. This means changes to the file are not instant. Apple re-crawls AASA files periodically (roughly every 24-48 hours, though Apple does not publish an exact schedule). During development, you can force a re-download using Apple&#39;s <a href=\"https:\/\/developer.apple.com\/documentation\/bundleresources\/applinks\/details\/components\" rel=\"nofollow noopener\" target=\"_blank\">developer mode<\/a>, but in production, expect a delay.<\/p>\n\n\n\n<p>For a conceptual overview, see <a href=\"https:\/\/tolinku.com\/docs\/concepts\/universal-links\/\">Tolinku&#39;s Universal Links documentation<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Setting Up Universal Links Step by Step<\/h2>\n\n\n\n<p>There are three things you need to configure: the AASA file on your server, the Associated Domains entitlement in Xcode, and the link-handling code in your app.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 1: Create the AASA File<\/h3>\n\n\n\n<p>Create a file named <code>apple-app-site-association<\/code> (no file extension) and place it at <code>\/.well-known\/apple-app-site-association<\/code> on your domain. The file must be served over HTTPS with a valid TLS certificate. No redirects. The content type should be <code>application\/json<\/code>.<\/p>\n\n\n\n<p>Here is a minimal AASA file:<\/p>\n\n\n\n<pre><code class=\"language-json\">{\n  &quot;applinks&quot;: {\n    &quot;details&quot;: [\n      {\n        &quot;appIDs&quot;: [&quot;ABCDE12345.com.example.myapp&quot;],\n        &quot;components&quot;: [\n          {\n            &quot;\/&quot;: &quot;\/product\/*&quot;,\n            &quot;comment&quot;: &quot;Matches product pages&quot;\n          },\n          {\n            &quot;\/&quot;: &quot;\/user\/*&quot;,\n            &quot;comment&quot;: &quot;Matches user profiles&quot;\n          }\n        ]\n      }\n    ]\n  }\n}\n<\/code><\/pre>\n\n\n\n<p>The <code>appIDs<\/code> value combines your <a href=\"https:\/\/developer.apple.com\/account\/\" rel=\"nofollow noopener\" target=\"_blank\">Apple Team ID<\/a> with your app&#39;s bundle identifier, separated by a period. You can find your Team ID in the Apple Developer portal under Membership.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 2: Configure Xcode<\/h3>\n\n\n\n<p>Open your project in Xcode and select your target. Go to &quot;Signing &amp; Capabilities&quot; and click &quot;+ Capability&quot; to add &quot;Associated Domains.&quot; Add an entry in this format:<\/p>\n\n\n\n<pre><code>applinks:yourdomain.com\n<\/code><\/pre>\n\n\n\n<p>If you want to test with a specific mode during development, you can append <code>?mode=developer<\/code>:<\/p>\n\n\n\n<pre><code>applinks:yourdomain.com?mode=developer\n<\/code><\/pre>\n\n\n\n<p>Developer mode tells iOS to fetch the AASA file directly from your server instead of Apple&#39;s CDN. This is useful during development because you do not have to wait for Apple to re-crawl your file after every change. Remove the <code>?mode=developer<\/code> flag before shipping to the App Store.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 3: Handle Incoming Links in Your App<\/h3>\n\n\n\n<p>When iOS opens your app from a universal link, it delivers the URL through the <code>NSUserActivity<\/code> continuation mechanism. How you receive it depends on your app&#39;s architecture.<\/p>\n\n\n\n<p>We will cover the code for each approach in the next section.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Apple App Site Association File in Detail<\/h2>\n\n\n\n<p>The AASA file is the most critical piece of the universal links setup. Get it wrong and nothing works. For a dedicated walkthrough, see the <a href=\"https:\/\/tolinku.com\/blog\/aasa-file-setup\/\">AASA file setup guide<\/a>. Here is what you need to know.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Format and Structure<\/h3>\n\n\n\n<p>The AASA file is JSON. Apple supports two formats: the legacy format (iOS 12 and earlier) and the modern format (iOS 13+). Use the modern format with <code>components<\/code> unless you need to support very old iOS versions.<\/p>\n\n\n\n<p>Here is a more complete example:<\/p>\n\n\n\n<pre><code class=\"language-json\">{\n  &quot;applinks&quot;: {\n    &quot;details&quot;: [\n      {\n        &quot;appIDs&quot;: [&quot;ABCDE12345.com.example.myapp&quot;],\n        &quot;components&quot;: [\n          {\n            &quot;\/&quot;: &quot;\/product\/*&quot;,\n            &quot;comment&quot;: &quot;All product pages&quot;\n          },\n          {\n            &quot;\/&quot;: &quot;\/invite\/*&quot;,\n            &quot;comment&quot;: &quot;Invitation links&quot;\n          },\n          {\n            &quot;\/&quot;: &quot;\/share\/*&quot;,\n            &quot;?&quot;: { &quot;ref&quot;: &quot;?*&quot; },\n            &quot;comment&quot;: &quot;Share links with referral parameter&quot;\n          },\n          {\n            &quot;\/&quot;: &quot;\/admin\/*&quot;,\n            &quot;exclude&quot;: true,\n            &quot;comment&quot;: &quot;Never open admin pages in the app&quot;\n          }\n        ]\n      }\n    ]\n  }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Path Matching<\/h3>\n\n\n\n<p>The <code>&quot;\/&quot;<\/code> key defines URL path patterns. You can use these wildcards:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>*<\/code> matches any substring within a single path segment. <code>\/product\/*<\/code> matches <code>\/product\/123<\/code> but not <code>\/product\/123\/reviews<\/code>.<\/li>\n<li><code>?<\/code> matches any single character. <code>\/user\/a?c<\/code> matches <code>\/user\/abc<\/code> but not <code>\/user\/abbc<\/code>.<\/li>\n<\/ul>\n\n\n\n<p>To match across multiple path segments, chain wildcards: <code>\/product\/*\/reviews<\/code> matches <code>\/product\/shoes\/reviews<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Excluding Paths<\/h3>\n\n\n\n<p>Set <code>&quot;exclude&quot;: true<\/code> on a component to prevent specific URL patterns from opening your app. This is useful for admin panels, login pages, or API endpoints that should always load in the browser.<\/p>\n\n\n\n<p>Apple <a href=\"https:\/\/developer.apple.com\/documentation\/bundleresources\/applinks\/details\/components\" rel=\"nofollow noopener\" target=\"_blank\">evaluates components in order<\/a>. Put your exclusion rules before your inclusion rules. The first matching rule wins.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Query Parameters and Fragments<\/h3>\n\n\n\n<p>The modern AASA format also lets you match on query parameters and URL fragments:<\/p>\n\n\n\n<pre><code class=\"language-json\">{\n  &quot;\/&quot;: &quot;\/product\/*&quot;,\n  &quot;?&quot;: { &quot;campaign&quot;: &quot;?*&quot; },\n  &quot;#&quot;: &quot;details&quot;,\n  &quot;comment&quot;: &quot;Product links with a campaign parameter and #details fragment&quot;\n}\n<\/code><\/pre>\n\n\n\n<p>The <code>&quot;?&quot;<\/code> key matches query string parameters. The <code>&quot;#&quot;<\/code> key matches the URL fragment. Both support the same wildcard syntax as paths.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Multiple Apps<\/h3>\n\n\n\n<p>You can list multiple apps in a single AASA file. Each entry in the <code>details<\/code> array can have a different <code>appIDs<\/code> value:<\/p>\n\n\n\n<pre><code class=\"language-json\">{\n  &quot;applinks&quot;: {\n    &quot;details&quot;: [\n      {\n        &quot;appIDs&quot;: [&quot;ABCDE12345.com.example.mainapp&quot;],\n        &quot;components&quot;: [{ &quot;\/&quot;: &quot;\/product\/*&quot; }]\n      },\n      {\n        &quot;appIDs&quot;: [&quot;ABCDE12345.com.example.liteapp&quot;],\n        &quot;components&quot;: [{ &quot;\/&quot;: &quot;\/lite\/*&quot; }]\n      }\n    ]\n  }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">File Serving Requirements<\/h3>\n\n\n\n<p>The AASA file must:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Be served over HTTPS (no HTTP, no self-signed certificates)<\/li>\n<li>Return <code>Content-Type: application\/json<\/code><\/li>\n<li>Not require authentication<\/li>\n<li>Not use redirects (Apple&#39;s CDN does not follow 3xx responses)<\/li>\n<li>Be less than 128 KB in size<\/li>\n<\/ul>\n\n\n\n<p>If you are using a CDN like <a href=\"https:\/\/developers.cloudflare.com\/fundamentals\/\" rel=\"nofollow noopener\" target=\"_blank\">Cloudflare<\/a> or <a href=\"https:\/\/www.fastly.com\/documentation\/\" rel=\"nofollow noopener\" target=\"_blank\">Fastly<\/a>, make sure the CDN does not block Apple&#39;s crawler or require a CAPTCHA challenge.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Handling Universal Links in Your App<\/h2>\n\n\n\n<p>Once iOS opens your app from a universal link, you need code to receive the URL and route the user to the right screen. Here are the three common patterns.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">UIKit with SceneDelegate (iOS 13+)<\/h3>\n\n\n\n<p>If your app uses <code>UISceneDelegate<\/code> (the default for apps created since iOS 13), implement the <code>scene(_:continue:)<\/code> method:<\/p>\n\n\n\n<pre><code class=\"language-swift\">import UIKit\n\nclass SceneDelegate: UIResponder, UIWindowSceneDelegate {\n    func scene(_ scene: UIScene,\n               continue userActivity: NSUserActivity) {\n        guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,\n              let url = userActivity.webpageURL else {\n            return\n        }\n        handleUniversalLink(url)\n    }\n\n    private func handleUniversalLink(_ url: URL) {\n        let path = url.path\n        let components = path.split(separator: &quot;\/&quot;)\n\n        if components.first == &quot;product&quot;,\n           let productID = components.last {\n            \/\/ Navigate to product screen\n            navigateToProduct(String(productID))\n        } else if components.first == &quot;user&quot;,\n                  let userID = components.last {\n            \/\/ Navigate to user profile\n            navigateToProfile(String(userID))\n        }\n    }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">UIKit with AppDelegate (Legacy)<\/h3>\n\n\n\n<p>For older apps that do not use scenes, implement <code>application(_:continue:restorationHandler:)<\/code>:<\/p>\n\n\n\n<pre><code class=\"language-swift\">import UIKit\n\n@UIApplicationMain\nclass AppDelegate: UIResponder, UIApplicationDelegate {\n    func application(_ application: UIApplication,\n                     continue userActivity: NSUserActivity,\n                     restorationHandler: @escaping ([UIUserActivityRestoring]?) -&gt; Void) -&gt; Bool {\n        guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,\n              let url = userActivity.webpageURL else {\n            return false\n        }\n        handleUniversalLink(url)\n        return true\n    }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">SwiftUI<\/h3>\n\n\n\n<p>In SwiftUI, use the <code>.onOpenURL<\/code> modifier. For a full SwiftUI integration guide, see <a href=\"https:\/\/tolinku.com\/blog\/universal-links-with-swiftui\/\">Universal Links with SwiftUI<\/a>.<\/p>\n\n\n\n<pre><code class=\"language-swift\">import SwiftUI\n\n@main\nstruct MyApp: App {\n    var body: some Scene {\n        WindowGroup {\n            ContentView()\n                .onOpenURL { url in\n                    handleUniversalLink(url)\n                }\n        }\n    }\n}\n<\/code><\/pre>\n\n\n\n<p>The <code>.onOpenURL<\/code> modifier handles both universal links and custom URL schemes. It is the simplest approach if you are building a new SwiftUI app. Apple documents this in the <a href=\"https:\/\/developer.apple.com\/documentation\/swiftui\/scene\/onopenurl(perform:)\" rel=\"nofollow noopener\" target=\"_blank\">SwiftUI App structure reference<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Parsing URLs Safely<\/h3>\n\n\n\n<p>Regardless of which delegate pattern you use, parse URLs defensively. Users (and attackers) can craft arbitrary URLs that match your AASA patterns. Validate every component:<\/p>\n\n\n\n<pre><code class=\"language-swift\">func handleUniversalLink(_ url: URL) {\n    guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {\n        return\n    }\n\n    let pathSegments = components.path\n        .split(separator: &quot;\/&quot;)\n        .map(String.init)\n\n    switch pathSegments.first {\n    case &quot;product&quot;:\n        guard pathSegments.count == 2,\n              let id = Int(pathSegments[1]) else { return }\n        router.navigate(to: .product(id: id))\n\n    case &quot;invite&quot;:\n        guard pathSegments.count == 2 else { return }\n        let token = pathSegments[1]\n        router.navigate(to: .invite(token: token))\n\n    default:\n        \/\/ Unknown path, open home screen\n        router.navigate(to: .home)\n    }\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Common Universal Links Problems and Fixes<\/h2>\n\n\n\n<p>Universal links have a reputation for being finicky. Most issues come down to one of a few root causes.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Problem: Links Open in Safari Instead of the App<\/h3>\n\n\n\n<p>This is the most common issue. Check these causes in order:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>AASA file not reachable.<\/strong> Verify your file is at <code>https:\/\/yourdomain.com\/.well-known\/apple-app-site-association<\/code>. Test it with curl:<\/li>\n<\/ol>\n\n\n\n<pre><code class=\"language-bash\">curl -v https:\/\/yourdomain.com\/.well-known\/apple-app-site-association\n<\/code><\/pre>\n\n\n\n<p>Confirm the response is valid JSON, the status code is 200 (not 301 or 302), and the <code>Content-Type<\/code> header is <code>application\/json<\/code>.<\/p>\n\n\n\n<ol class=\"wp-block-list\" start=\"2\">\n<li><p><strong>Team ID or Bundle ID mismatch.<\/strong> The <code>appIDs<\/code> value must exactly match <code>{TeamID}.{BundleID}<\/code>. Check both values in the <a href=\"https:\/\/developer.apple.com\/account\/\" rel=\"nofollow noopener\" target=\"_blank\">Apple Developer portal<\/a>.<\/p>\n\n\n\n<p><strong>Associated Domains entitlement missing.<\/strong> Open your <code>.entitlements<\/code> file and confirm <code>applinks:yourdomain.com<\/code> is listed. Also check that the Associated Domains capability is enabled in the Apple Developer portal under &quot;Certificates, Identifiers &amp; Profiles.&quot;<\/p>\n\n\n\n<p><strong>Apple CDN has not updated.<\/strong> After changing your AASA file, Apple&#39;s CDN may take 24-48 hours to re-fetch it. During development, use <code>?mode=developer<\/code> on your associated domain to bypass the CDN. For details on CDN behavior, see <a href=\"https:\/\/tolinku.com\/blog\/apple-cdn-validation-universal-links\/\">Apple CDN validation for Universal Links<\/a>.<\/p>\n\n\n\n<p><strong>User previously dismissed the link.<\/strong> If a user taps-and-holds a universal link and chooses &quot;Open in Safari,&quot; iOS remembers that choice for your domain. The user has to long-press the link again and choose &quot;Open in [Your App]&quot; to restore the behavior.<\/p>\n\n\n\n<p>For a full troubleshooting checklist, see <a href=\"https:\/\/tolinku.com\/docs\/troubleshooting\/ios\/\">Tolinku&#39;s iOS troubleshooting guide<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Problem: AASA File Validation Errors<\/h3>\n\n\n\n<p>Apple provides a <a href=\"https:\/\/search.developer.apple.com\/appsearch-validation-tool\/\" rel=\"nofollow noopener\" target=\"_blank\">validation tool<\/a> (though it is not always reliable). Common issues:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Trailing commas in JSON.<\/strong> Apple&#39;s parser is strict. Run your file through <code>python3 -m json.tool<\/code> or <a href=\"https:\/\/jsonlint.com\/\" rel=\"nofollow noopener\" target=\"_blank\">jsonlint.com<\/a> to validate.<\/li>\n<li><strong>Wrong Content-Type.<\/strong> Some web servers serve files without an extension as <code>application\/octet-stream<\/code>. Configure your server to return <code>application\/json<\/code> for this specific path.<\/li>\n<li><strong>File too large.<\/strong> Keep your AASA file under 128 KB. If you have many paths, consider consolidating them with wildcard patterns.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Problem: Links Work in Some Apps but Not Others<\/h3>\n\n\n\n<p>Some apps (particularly social media apps) use in-app browsers that do not support universal links. Links tapped inside Instagram&#39;s in-app browser, for example, may load in the embedded web view instead of handing off to your app. This is a known limitation.<\/p>\n\n\n\n<p>The workaround: on your web page, include a smart banner or a button that explicitly opens the universal link. The <code>meta<\/code> tag for <a href=\"https:\/\/developer.apple.com\/documentation\/webkit\/promoting_apps_with_smart_app_banners\" rel=\"nofollow noopener\" target=\"_blank\">Safari Smart App Banners<\/a> can help:<\/p>\n\n\n\n<pre><code class=\"language-html\">&lt;meta name=&quot;apple-itunes-app&quot; content=&quot;app-id=123456789, app-argument=https:\/\/yourdomain.com\/product\/123&quot;&gt;\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Problem: Universal Links Break After App Update<\/h3>\n\n\n\n<p>If your Team ID or Bundle ID changes between app versions, or if you change your domain, universal links will break. Make sure the AASA file on your server always matches the entitlements in your currently shipped app binary.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Testing Universal Links<\/h2>\n\n\n\n<p>Testing universal links takes some care because you cannot just tap a link in Safari&#39;s address bar. For a comprehensive testing guide, see <a href=\"https:\/\/tolinku.com\/blog\/testing-universal-links\/\">testing Universal Links<\/a>. Here are reliable methods.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">On a Real Device<\/h3>\n\n\n\n<p>The most reliable test. Type your universal link into the Notes app, then tap it. If the app opens, universal links are working.<\/p>\n\n\n\n<p>You can also use the terminal to open a link on a connected device:<\/p>\n\n\n\n<pre><code class=\"language-bash\">xcrun simctl openurl booted &quot;https:\/\/yourdomain.com\/product\/123&quot;\n<\/code><\/pre>\n\n\n\n<p>This command works with the iOS Simulator. For a physical device, paste the link into Notes or Messages and tap it.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Apple&#39;s CDN Diagnostic Tool<\/h3>\n\n\n\n<p>Check what Apple&#39;s CDN has cached for your domain:<\/p>\n\n\n\n<pre><code class=\"language-bash\">curl -v &quot;https:\/\/app-site-association.cdn-apple.com\/a\/v1\/yourdomain.com&quot;\n<\/code><\/pre>\n\n\n\n<p>This returns the AASA file as Apple&#39;s CDN sees it. If the response does not match your server&#39;s file, Apple has not re-crawled it yet.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Using Console.app<\/h3>\n\n\n\n<p>Connect your iOS device to your Mac and open Console.app. Filter for <code>swcd<\/code> (the daemon responsible for associated domains). You will see log messages showing whether iOS successfully validated your AASA file and whether incoming URLs matched any patterns.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Automated Validation<\/h3>\n\n\n\n<p>You can validate your AASA file programmatically as part of your CI\/CD pipeline:<\/p>\n\n\n\n<pre><code class=\"language-bash\"># Fetch and validate the AASA file\nAASA=$(curl -s https:\/\/yourdomain.com\/.well-known\/apple-app-site-association)\n\n# Check it&#39;s valid JSON\necho &quot;$AASA&quot; | python3 -m json.tool &gt; \/dev\/null 2&gt;&amp;1\nif [ $? -ne 0 ]; then\n  echo &quot;ERROR: AASA file is not valid JSON&quot;\n  exit 1\nfi\n\n# Check for the applinks key\necho &quot;$AASA&quot; | python3 -c &quot;import sys, json; d = json.load(sys.stdin); assert &#39;applinks&#39; in d&quot; 2&gt;\/dev\/null\nif [ $? -ne 0 ]; then\n  echo &quot;ERROR: AASA file missing &#39;applinks&#39; key&quot;\n  exit 1\nfi\n\necho &quot;AASA file is valid&quot;\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Universal Links vs. Custom URL Schemes<\/h2>\n\n\n\n<p>Both open your app from a URL, but they work differently and have different tradeoffs.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Feature<\/th>\n<th>Universal Links<\/th>\n<th>Custom URL Schemes<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>URL format<\/td>\n<td><code>https:\/\/yourdomain.com\/path<\/code><\/td>\n<td><code>yourapp:\/\/path<\/code><\/td>\n<\/tr>\n<tr>\n<td>Requires server<\/td>\n<td>Yes (AASA file)<\/td>\n<td>No<\/td>\n<\/tr>\n<tr>\n<td>Fallback if app not installed<\/td>\n<td>Opens in Safari<\/td>\n<td>Error or nothing<\/td>\n<\/tr>\n<tr>\n<td>Unique to your app<\/td>\n<td>Yes (domain verified)<\/td>\n<td>No (any app can claim the scheme)<\/td>\n<\/tr>\n<tr>\n<td>Works in all contexts<\/td>\n<td>Most (some in-app browsers excluded)<\/td>\n<td>Yes<\/td>\n<\/tr>\n<tr>\n<td>Requires Apple Developer account<\/td>\n<td>Yes<\/td>\n<td>No<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<p>Use universal links for anything user-facing: marketing links, emails, QR codes, social sharing. Use custom URL schemes for app-to-app communication on the same device or for OAuth callback URLs where you control both ends. For a deeper comparison, see <a href=\"https:\/\/tolinku.com\/blog\/uri-schemes-vs-universal-links\/\">URI schemes vs Universal Links<\/a>.<\/p>\n\n\n\n<p>Apple&#39;s documentation on <a href=\"https:\/\/developer.apple.com\/documentation\/xcode\/defining-a-custom-url-scheme-for-your-app\" rel=\"nofollow noopener\" target=\"_blank\">defining a custom URL scheme<\/a> covers the custom scheme approach if you need it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Universal Links with Tolinku<\/h2>\n\n\n\n<p>Setting up universal links from scratch involves hosting the AASA file, keeping it in sync with your app&#39;s entitlements, and managing paths across environments. <a href=\"https:\/\/tolinku.com\/features\/deep-linking\">Tolinku<\/a> simplifies this by hosting the AASA file for you and providing a dashboard where you can manage your link <a href=\"https:\/\/tolinku.com\/docs\/user-guide\/routes\/\">routes<\/a> without touching server configuration.<\/p>\n\n\n\n<p>When you configure an Appspace in Tolinku, the platform generates and serves the AASA file automatically based on the routes you define. You point your Associated Domains entitlement to the Tolinku-managed domain, and the platform handles the rest: path matching, fallback behavior, analytics, and AASA file updates.<\/p>\n\n\n\n<p>For step-by-step setup instructions, see the <a href=\"https:\/\/tolinku.com\/docs\/developer\/universal-links\/\">Tolinku developer guide for universal links<\/a> and the <a href=\"https:\/\/tolinku.com\/docs\/user-guide\/configuring-ios\/\">iOS configuration guide<\/a>.<\/p>\n\n\n\n<p>This approach is especially useful if you manage multiple apps or need to update link routing without shipping a new app version. The AASA file updates are immediate because you control the hosting, and there is no waiting for Apple&#39;s CDN to re-crawl your personal server (though the CDN caching delay still applies on Apple&#39;s end).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Best Practices<\/h2>\n\n\n\n<p>Here are the patterns that save time and prevent headaches:<\/p>\n\n\n\n<p><strong>Keep your AASA file simple.<\/strong> Fewer, broader path patterns are easier to maintain than dozens of specific ones. Use wildcards generously. <code>\/product\/*<\/code> is better than listing every product ID.<\/p>\n\n\n\n<p><strong>Version your AASA file.<\/strong> Track it in source control alongside your app code. When your app&#39;s URL handling changes, the AASA file should change in the same pull request.<\/p>\n\n\n\n<p><strong>Test on real devices before every release.<\/strong> The iOS Simulator does not always behave identically to real hardware for universal links. Budget time for manual testing on a physical iPhone.<\/p>\n\n\n\n<p><strong>Handle fallbacks gracefully.<\/strong> Your web page at the universal link URL should show useful content, not just a &quot;download our app&quot; splash page. Some users will land there because they do not have the app, because they are on a desktop browser, or because an in-app browser intercepted the link. Give them a good web experience too.<\/p>\n\n\n\n<p><strong>Monitor your AASA file availability.<\/strong> If your server goes down or your CDN misconfigures the AASA path, universal links will silently stop working for new installs. Set up uptime monitoring for <code>\/.well-known\/apple-app-site-association<\/code>.<\/p>\n\n\n\n<p><strong>Do not rely on universal links for critical flows.<\/strong> Because users can manually override universal link behavior (by choosing to open in Safari), your app should have alternative ways for users to get to important screens. Deep link handling should enhance the experience, not be the only path.<\/p>\n\n\n\n<p><strong>Use the <code>?mode=developer<\/code> flag during development.<\/strong> It saves significant time by bypassing Apple&#39;s CDN cache. Just remember to remove it before submitting to the App Store.<\/p>\n\n\n\n<p><strong>Test across iOS versions.<\/strong> The AASA format and universal link behavior have changed across iOS 12, 13, 14, and later versions. If you support older iOS versions, test on each one. The <a href=\"https:\/\/developer.apple.com\/documentation\/xcode\/supporting-universal-links-in-your-app\" rel=\"nofollow noopener\" target=\"_blank\">Apple developer documentation on supporting universal links<\/a> covers version-specific differences.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>Universal links are the correct way to open your iOS app from a URL. They are more secure than custom URL schemes, provide a built-in web fallback, and give users a smooth transition from the web to your app. The setup involves three pieces: a JSON file on your server, an entitlement in Xcode, and a few lines of Swift to handle incoming URLs.<\/p>\n\n\n\n<p>The most common stumbling blocks (AASA file formatting, CDN caching delays, server misconfiguration) are all solvable once you know what to look for. Test early, test on real devices, and monitor your AASA file in production.<\/p>\n\n\n\n<p>If you want to skip the manual AASA hosting and manage your link routing through a dashboard, <a href=\"https:\/\/tolinku.com\/features\/deep-linking\">Tolinku&#39;s deep linking platform<\/a> handles the infrastructure so you can focus on building your app.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The definitive guide to Apple Universal Links. Learn setup, configuration, testing, and troubleshooting for iOS deep linking in 2026.<\/p>\n","protected":false},"author":2,"featured_media":363,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Universal Links: Everything You Need to Know in 2026","rank_math_description":"The definitive guide to Apple Universal Links. Learn setup, configuration, testing, and troubleshooting for iOS deep linking in 2026.","rank_math_focus_keyword":"universal links","rank_math_canonical_url":"","rank_math_facebook_title":"","rank_math_facebook_description":"","rank_math_facebook_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-universal-links-everything-you-need-to-know.png","rank_math_facebook_image_id":"","rank_math_twitter_title":"","rank_math_twitter_description":"","rank_math_twitter_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-universal-links-everything-you-need-to-know.png","footnotes":""},"categories":[12],"tags":[32,20,24,30,31,22,33],"class_list":["post-364","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ios","tag-apple-app-site-association","tag-deep-linking","tag-ios","tag-sdks","tag-swift","tag-universal-links","tag-user-experience"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/364","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/comments?post=364"}],"version-history":[{"count":2,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/364\/revisions"}],"predecessor-version":[{"id":2761,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/364\/revisions\/2761"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/363"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=364"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=364"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=364"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}