Universal Links not working? An iOS debugging checklist
When a link opens Safari instead of your app, the cause is in one of four places: the AASA file, Apple's CDN, your app's entitlement, or how you're testing. Here's how to check each one.
You tap a link and Safari opens instead of your app. There's no error message: iOS falls back to the browser without saying why. That's why Universal Links are hard to debug.
The good news is that the cause is almost always in one of four places:
- The
apple-app-site-association(AASA) file on your domain. - Apple's CDN, which is where iPhones actually read that file.
- Your app: the Associated Domains entitlement and the code that receives the link.
- How you're testing: some ways of opening a link never open an app.
Work through them in order. Each section ends with a command or a setting you can check in a minute or two.
How Universal Links decide whether to open your app
Here's what iOS does:
- When your app is installed (and periodically after that), iOS reads the AASA file for every
applinks:domain in your app's entitlement. Since iOS 14 it reads the file from Apple's CDN, not from your server. - When someone taps an
https://link on one of those domains, iOS checks the path against thecomponentsin the file. If it matches and the user hasn't opted out, the app opens. Otherwise, the browser opens.
So a link can only open your app if all of these are true: the file is reachable and valid, Apple's CDN has a current copy, the app lists the exact domain, and the path matches. If one is false, the link opens in Safari.
1. Check the apple-app-site-association file
Start with the raw response, the way Apple's crawler sees it:
curl -v https://links.example.com/.well-known/apple-app-site-association
Check each of these:
- HTTP 200, over HTTPS, with a valid certificate. A self-signed or incomplete certificate chain fails silently.
- No redirects. A 301 or 302 (for example,
example.com→www.example.com, or a login wall) means the file isn't used. Host the file on every domain and subdomain you list in the app. - The exact path.
/.well-known/apple-app-site-association, with no.jsonextension. - JSON content type. Serve it as
application/json. A common trap: single-page apps return theirindex.htmlwith a 200 for unknown paths, so the "file" is really HTML. - No IP or user-agent blocking. Apple's technote says the file must be reachable from any IP address and any user agent. Bot protection (Cloudflare, AWS WAF, "under attack" modes) can block Apple's crawler while your browser works fine. Test with a random user agent:
curl -A "MyAgent-Bot/1.0" https://links.example.com/.well-known/apple-app-site-association
What a correct AASA file looks like
Apple recommends the appIDs + components format (iOS 13 and later):
{
"applinks": {
"details": [
{
"appIDs": ["ABCDE12345.com.example.shop"],
"components": [
{ "/": "/product/*", "comment": "Product pages" },
{ "/": "/account/*", "exclude": true, "comment": "Keep account pages in the browser" },
{ "/": "/*" }
]
}
]
}
}
Things that commonly go wrong in the file:
- Wrong app ID. It's
TEAMID.bundleid. The Team ID is the 10-character ID under Membership details on developer.apple.com, not your name and not the App Store ID. The bundle ID must match the build you installed, including.devor.stagingsuffixes. - Mixing formats. Use either
appIDs+components, or the olderappID+paths. Apple advises against mixing them in one entry. - Order matters. Components are checked top to bottom and the first match wins. Put an
excluderule before a catch-all/*, or the catch-all matches first. - Patterns that don't match your real URLs.
/product/*doesn't match/products/42. Paste a full link into a validator rather than reading patterns by eye.
Verify the file and a path with swcutil (macOS)
macOS ships Apple's own tool for this. Download the file and test a URL against it:
sudo swcutil dl -d links.example.com
curl -s https://links.example.com/.well-known/apple-app-site-association -o aasa.json
sudo swcutil verify -d links.example.com -j ./aasa.json -u https://links.example.com/product/42
A matching path prints Pattern "https://links.example.com/product/42" matched. An excluded one prints blocked match. If a URL you expect to open the app says neither, your components don't cover it.
2. Check what Apple's CDN has
This is the step most guides skip, and it explains a lot of "the file is fine but it still doesn't work" reports.
Since iOS 14, devices don't download the AASA file from your server. They get it from an Apple-managed CDN, which fetches your file and caches it. You can see exactly what devices see:
curl -s https://app-site-association.cdn-apple.com/a/v1/links.example.com
Compare that with your server's file. If they differ, phones are still using the old copy. According to Apple's technote, devices download the file when the app is installed and check for updates about once a week after that. There's no way to purge the CDN. To pick up a new version on a test device, delete and reinstall the app once the CDN has the new file.
If the CDN returns an error or nothing at all, it couldn't fetch your file. Go back to step 1 and look at redirects, firewalls and certificates.
Skipping the CDN during development
When you're testing against a staging server, or changing the file faster than the CDN refreshes, use developer mode:
- Add
?mode=developerto the entitlement:applinks:staging.example.com?mode=developer. - On the iPhone, turn on Developer Mode, then go to Settings → Developer and turn on Associated Domains Development.
- Build and run from Xcode. The device now fetches the file directly from your server.
Remove ?mode=developer before you submit to the App Store; it's for development builds only.
3. Check the app: entitlement and link handling
The Associated Domains entitlement
In Xcode: your target → Signing & Capabilities → Associated Domains. Each domain needs its own line:
applinks:links.example.com
applinks:example.com
Common mistakes:
- Including the protocol. It's
applinks:links.example.com, notapplinks:https://links.example.com. - Expecting the root domain to cover subdomains.
applinks:example.comdoesn't coverwww.example.com. List each subdomain, or useapplinks:*.example.com, which reads the file from the root domain. - Using a port.
applinks:example.com:8080never works. - The capability isn't in the signed build. It's easy to add it to one target or configuration and ship another. Check the actual build:
codesign -d --entitlements :- /path/to/YourApp.app
Look for com.apple.developer.associated-domains with your domains in the output. The App ID on developer.apple.com also needs the Associated Domains capability turned on, and your provisioning profile must include it.
Receiving the link in code
If the app opens but lands on the home screen, the link reached iOS but not your code.
- SwiftUI: use
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in ... }and readactivity.webpageURL. - UIKit with scenes: links arrive in
scene(_:continue:)and, on a cold start, inconnectionOptions.userActivitiesinsidescene(_:willConnectTo:options:). Implementing only the AppDelegate method is a classic bug in apps that use scenes. - React Native: forward the activity to
RCTLinkingManagerin your AppDelegate, then read the URL withLinking.getInitialURL()andLinking.addEventListener("url", ...):
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
return RCTLinkingManager.application(
application, continue: userActivity, restorationHandler: restorationHandler)
}
Our React Native deep linking guide covers the JavaScript side.
4. Check how you're testing
Several ways of "testing a link" never open an app, even when everything is set up correctly:
- Typing or pasting the URL into Safari's address bar. That's direct navigation in the browser, and it never opens the app.
- Tapping a link on the same domain. If you're already on
example.comin Safari and tap anotherexample.comlink, Safari keeps you in the browser. Apple suggests a separate subdomain (such aslinks.example.com) for links that should open the app from your own site. - The user opted out. If you long-press a link and choose "Open in Safari", iOS remembers that choice for the domain. Long-press a link again and choose "Open in [App]" to reset it.
- In-app browsers. Instagram, Facebook and some other apps open links in their own web view, which often doesn't trigger Universal Links. You need a fallback page with an "Open in app" button for those.
The reliable test: paste the link into Notes or Messages, then tap it. If you long-press it, you should see an option to open it in your app.
Use the on-device diagnostics
iOS has a built-in checker. With Developer Mode on, go to Settings → Developer → Universal Links → Diagnostics and enter your full URL. It tells you whether the link is valid for an installed app.
For harder cases, Apple's technote suggests a sysdiagnose: open swcutil_show.txt and search for your app ID. Site/Fmwk Approval: approved means the CDN approved the association. denied or unspecified points back to hosting or the entitlement. User Approval shows whether the user opted out.
A 2-minute checklist
| Check | How |
|---|---|
| File returns 200, JSON, no redirect | curl -v https://domain/.well-known/apple-app-site-association |
App ID is TEAMID.bundleid for this build |
Membership details + Xcode bundle ID |
Your path matches components |
sudo swcutil verify -d … -j … -u … |
| Apple's CDN has the current file | curl https://app-site-association.cdn-apple.com/a/v1/domain |
Entitlement lists the exact domain, no https:// |
codesign -d --entitlements :- YourApp.app |
| App was reinstalled after the file changed | Delete and reinstall |
| Tested by tapping, not typing | Notes or Messages |
| Code handles the link (scene / RN Linking) | Log webpageURL |
Let a tool check it for you
Our free Universal Link & App Link Validator runs most of these server-side checks at once: HTTPS, redirects, content type, JSON structure, app ID format, whether your exact path matches, and whether Apple's CDN copy matches your server. It also checks Android's assetlinks.json; if Android is giving you trouble too, see Android App Links not verified? Fix assetlinks.json.
If you'd rather not host and maintain these files at all, CodeOnAir AppLinks builds apple-app-site-association and assetlinks.json from your app details and serves them on every link domain, your free yourapp.codeonair.com subdomain or your own domain. Adding ?d=1 to any link opens a link debugger that shows where each platform would go. The AppLink setup docs walk through it. AppLinks is on the paid plans (pricing); sign-ups are currently by invitation, so request access.
Need a tester's device ID for an ad hoc build while you're at it? Get UDID finds it in Safari.
FAQ
Why do my Universal Links open in Safari instead of the app?
iOS falls back to Safari when it can't confirm the association. That happens when the AASA file is unreachable, redirects or is invalid, when Apple's CDN has an old copy, when the app's Associated Domains entitlement doesn't list that exact domain, or when the path isn't in components. It also happens if you type the URL into Safari, or if the user once chose "Open in Safari" for that domain.
How long does Apple take to pick up a new apple-app-site-association file?
Apple's CDN fetches and caches the file; there's no manual purge. Devices download it when the app is installed and check about once a week after that. For testing, reinstall the app after the CDN has the new version, or use ?mode=developer with Associated Domains Development turned on.
Does the apple-app-site-association file need a .json extension?
No. Serve it at /.well-known/apple-app-site-association with no extension, over HTTPS, with an application/json content type and no redirects.
Do I need an AASA file on www and the root domain?
Yes, if you link from both. Each domain or subdomain listed in your entitlement needs its own file at its own /.well-known/ path. The exception is a wildcard entry (applinks:*.example.com), which reads the file from the root domain.
Can I test Universal Links in the iOS Simulator?
Yes. Run xcrun simctl openurl booted https://links.example.com/product/42 with your app installed, or tap the link in Notes in the simulator. The simulator also reads the file through Apple's CDN unless you use ?mode=developer.
Why do Universal Links work in Notes but not in Instagram?
Instagram and Facebook open links in their own in-app browser, which often doesn't hand Universal Links to iOS. A landing page with an "Open in app" button (or a custom URI scheme) is the usual workaround.
Checked 2026-10-22 against Apple's TN3155 "Debugging universal links" (revision dated 2025-05-15) and "Supporting associated domains". iOS behavior can change between releases; if a step doesn't match what you see, Apple's technote is the reference.