Universal Links not opening your app: a checklist that finds the cause
Your link opens Safari instead of your iPhone app. Work through these checks in order, from the apple-app-site-association file to in-app browsers, and you will find which one is failing.
· DL2App · 6 min read
You tap a link to your own domain on an iPhone and Safari opens instead of your app. Universal Links fail silently: there is no error message, just the wrong app. The cause is almost always one of a short list, and they can be checked in order.
Start at the top. Each step assumes the ones before it pass.
1. The apple-app-site-association file is reachable
iOS decides whether a domain may open your app by reading a file at:
https://link.yourapp.com/.well-known/apple-app-site-association
Open that address in a browser and check:
- It answers 200 directly. No redirect of any kind, not even
httptohttpsor the bare domain towww. Apple does not follow redirects for this file. - It is served over HTTPS with a valid certificate for that exact hostname.
- It is JSON, sent as
application/json, with no.jsonon the end of the filename. - It is not behind a login, a cookie wall, a bot check or a firewall rule. Cloudflare's bot protection and "under attack" modes are a common culprit, because Apple's servers are not a browser.
- It is small. Keep it well under 128 KB.
Every hostname needs its own file. yourapp.com and www.yourapp.com are two domains to iOS.
2. The file names your app correctly
A minimal file looks like this:
{
"applinks": {
"details": [
{
"appIDs": ["ABCDE12345.com.yourcompany.yourapp"],
"components": [
{ "/": "/_/*", "exclude": true },
{ "/": "*" }
]
}
]
}
}
- The app ID is your Team ID, a dot, then the bundle ID. The Team ID is the ten-character code in your Apple Developer account under Membership. It is not your App Store ID and not your name.
- The bundle ID matches the build exactly, including any
.devor.stagingsuffix if you are testing one of those builds. - The path you are tapping is covered by
components, and not caught by an earlier"exclude": truerule. Rules are read top to bottom; the first match wins.
3. Apple has the current version of the file
Since iOS 14, phones don't fetch the file from your server. Apple's CDN fetches it and phones ask Apple. That has two effects:
- Changes take time to arrive. After you fix the file, the CDN can keep serving the old one for a while. The current copy Apple holds is at
https://app-site-association.cdn-apple.com/a/v1/link.yourapp.com. If that still shows the old file, wait. - The file is read when the app is installed or updated. After a fix, delete the app and install it again before you test.
While developing, you can make a test device fetch the file directly by adding ?mode=developer to the domain in your entitlements (applinks:link.yourapp.com?mode=developer) and turning on Associated Domains Development under Settings › Developer on the phone.
4. The app build declares the domain
In Xcode, open your target, Signing & Capabilities, and check Associated Domains lists:
applinks:link.yourapp.com
- No
https://and no path, just the hostname. - One line per hostname, including any second host you use for hand-offs and any custom domain.
- The capability is in the provisioning profile. If you add it in Xcode but sign with an old profile, the build silently ships without it. Look for
com.apple.developer.associated-domainsin the built app's entitlements.
5. Your app passes the link to your code
If the app opens but lands on its home screen, iOS did its part. The link reached your app and was dropped.
- Native iOS: handle
NSUserActivitywith the typeNSUserActivityTypeBrowsingWebinscene(_:continue:)(orapplication(_:continue:restorationHandler:)without scenes). A cold start delivers it inconnectionOptionsinstead. - React Native: the template doesn't forward Universal Links to JavaScript. Add
RCTLinkingManager'sapplication(_:continue:restorationHandler:)to yourAppDelegate, then read the link withLinking.getInitialURL()and theurlevent. - Flutter: use a package that receives Universal Links (such as
app_links) and read the initial link on a cold start as well as later ones.
6. How the link was tapped
This is where most "it works for me but not for them" reports come from. A correctly set-up Universal Link still opens Safari when:
- The link is typed or pasted into Safari's address bar. Universal Links only fire from a tap.
- The person is already on that domain in Safari. A tap from
link.yourapp.com/atolink.yourapp.com/bstays in the browser. iOS opens the app on taps that cross from one domain to another. This is why link services keep a second hostname for pages that hand people on to the app. - The link arrives through a redirect from another domain. Email click tracking, link shorteners and ad trackers wrap your link in their own address. The tap is on their domain, which redirects to yours, and iOS often won't open the app at the end of that chain.
- The person once chose to open it in Safari. iOS remembers. Long-press the link and choose Open in "Your App" to switch it back.
- The tap happens inside Instagram, Facebook, TikTok or another app's own browser. Many of these load the link inside their web view and never hand it to iOS. The person needs a way out to Safari or the app.
- The app is not installed. Then Safari is the correct result, and what happens next depends on where that page sends them.
7. Test it properly
- Put the link in Notes or Messages and tap it there. That is the cleanest test of the iOS side.
- On iOS 16 and later, Settings › Developer › Universal Links › Diagnostics lets you enter a URL and shows whether the installed app claims it.
- Test the shapes people actually use: from WhatsApp, from Instagram, from an email, app installed and not installed.
Check it in one go
Our free link checker runs the server-side half of this list for any link: it fetches the apple-app-site-association and assetlinks.json files, follows every redirect, checks status codes, content types and app IDs, and shows what iPhone, Android, desktop and in-app browsers are sent. If you are writing the file by hand, the AASA generator produces one from your Team ID and bundle ID.
Or let a link service host it
The server half of this checklist is what a deep link service does for you. With DL2App, each project's hosts and custom domains serve an apple-app-site-association file generated from the app details you enter once, without redirects, plus a second hostname for hand-offs between domains. Pages inside Instagram and other in-app browsers help people get out to the app. Health checks re-test the files on a schedule and email you if they break. The app side (steps 4 and 5) is still yours, and the setup guide walks through it for native iOS, React Native and Expo.
Questions people ask
Why does my Universal Link open in Safari instead of the app?
Usually one of three things: the apple-app-site-association file can't be fetched cleanly (a redirect, wrong content type or a bot check), the build doesn't list the domain under Associated Domains, or the link was tapped in a way iOS ignores (typed in the address bar, the same domain, or inside an in-app browser).
How long until a fixed apple-app-site-association file takes effect? Apple's CDN has to fetch the new version, and the app reads it on install or update. Check the CDN's copy, then reinstall the app on your test phone.
Do Universal Links work in the simulator? Yes, for testing the app side. The domain still has to serve a valid file over HTTPS.
Do Universal Links work in Instagram or Facebook? Often not: those apps open links in their own browser. Give people a visible way to continue in Safari or open the app.
Start with one link
The free plan has no time limit and needs no card. Create a project, add your app, and send yourself the link.