Why Your AASA and assetlinks.json Verification Keeps Failing (And How to Fix Every Cause)
You set up Universal Links on iOS or App Links on Android, you followed the guide, and the link still opens in the browser instead of your app. This is the single most common wall developers hit with deep linking, and almost every cause comes down to one of a handful of issues with your apple-app-site-association (AASA) file or your assetlinks.json file.
This is a practical checklist. Work through it top to bottom. Each section is a real cause, how to confirm it's your problem, and how to fix it.
First, understand what verification actually does
When your app installs, the operating system fetches a small JSON file from your domain to confirm that your app is allowed to handle links for that domain:
- iOS fetches
https://yourdomain.com/.well-known/apple-app-site-association - Android fetches
https://yourdomain.com/.well-known/assetlinks.json
If that fetch fails, or the file is malformed, or the contents don't match your app exactly, the OS silently falls back to opening the link in the browser. There is rarely a clear error message. That silence is why this is so frustrating to debug, and why a checklist beats guesswork.
Cause 1: The file is served over a redirect
This is the most common cause, and the easiest to miss.
Both iOS and Android require the file to be served from the exact domain with a 200 status code. No 301, no 302. If your CDN, hosting provider, or framework adds a redirect (for example, redirecting yourdomain.com to www.yourdomain.com, or forcing a trailing slash), verification fails even though the file loads fine in a browser.
How to confirm:
curl -I https://yourdomain.com/.well-known/apple-app-site-association
Look at the status line. If you see 301 or 302 anywhere in the chain, that's your problem.
How to fix: Serve the file directly from the apex or subdomain you actually associated in your app, with a clean 200. Make sure no global redirect rule (often a default in Cloudflare, Vercel, Netlify, or Nginx) intercepts the .well-known path.
Cause 2: Wrong content type or a file extension
The AASA file must be served as application/json and must have no file extension. It is apple-app-site-association, not apple-app-site-association.json.
A surprising number of hosting setups either append an extension automatically or refuse to serve an extensionless file with the right content type.
How to confirm:
curl -I https://yourdomain.com/.well-known/apple-app-site-association
Check the Content-Type header. It should be application/json.
How to fix: Configure your server to serve the extensionless file as JSON. On Android, assetlinks.json does keep its extension and must also be served as application/json.
Cause 3: The JSON is malformed or has the wrong structure
A single misplaced comma breaks the whole file, and the OS won't tell you why.
For iOS, a minimal valid AASA looks like this:
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAMID.com.yourcompany.yourapp",
"paths": ["*"]
}
]
}
}
For Android, a minimal valid assetlinks.json:
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.yourcompany.yourapp",
"sha256_cert_fingerprints": ["AB:CD:EF:..."]
}
}
]
How to fix: Validate the JSON. A quick way to catch syntax errors:
curl -s https://yourdomain.com/.well-known/apple-app-site-association | python3 -m json.tool
If that command errors, your JSON is malformed.
Cause 4: The App ID or package name doesn't match exactly
iOS verification fails if the appID doesn't match TEAMID.bundleID exactly. The Team ID prefix is required, and the bundle ID must match character for character, including case.
Android verification fails if either the package_name or the sha256_cert_fingerprints don't match the app that's actually installed.
The fingerprint trap on Android: The most common Android cause is a signing key mismatch. If you use Google Play App Signing, the fingerprint in assetlinks.json must be the one Google re-signs with, not your local upload key. These are different keys. Using the upload key fingerprint when Play re-signs your app is a guaranteed verification failure in production, even though it works in your local debug build.
How to fix: Get the correct release fingerprint from the Google Play Console (App Signing section) and make sure it's the one in your file. For iOS, copy your Team ID directly from your Apple Developer account and your bundle ID directly from Xcode, rather than typing them.
Cause 5: Apple's CDN cached an old version
Apple does not fetch your AASA file directly from your server every time. It goes through a CDN that caches the file and refreshes periodically, which can take up to 24 hours. So if you fixed your file an hour ago and it's still not working, the OS may still be holding the old, broken version.
How to fix while testing: On a development device, you can enable the associated domains developer mode setting so the device fetches the file directly and bypasses the CDN cache. Alternatively, uninstall and reinstall the app to force a fresh fetch. In production, just be aware that changes are not instant, version your file changes, and test thoroughly before shipping.
Cause 6: Path matching rules don't include your link
Your file can be perfectly valid and still not open the app if the paths (iOS) or path filters (Android) don't match the specific URL you're testing.
Path matching is case-sensitive on most servers. /Product/123 and /product/123 are treated as different paths. If your file allows /product/* but your link uses /Product/123, it won't match.
How to fix: While debugging, widen your paths to ["*"] on iOS to confirm the rest of the setup works, then narrow them back down once links open reliably. Normalize your URLs to a consistent case.
Cause 7: You're testing on a simulator or emulator
Universal Links and App Links often do not verify reliably on simulators and emulators. Domain verification frequently fails there even when your setup is correct.
How to fix: Always test on a physical device. To inspect the actual verification state:
- iOS: use the
swcutilcommand-line tool on macOS to check the associated domains status. - Android: run
adb shell pm get-app-links com.yourcompany.yourappto see whether the domain is verified.
A faster way to check all of this
Manually curling headers and eyeballing JSON works, but it's slow and easy to miss something. We built free validators that run the full set of checks for you, no login required:
- AASA Validator runs through the iOS-side checks above (reachability, status code, content type, JSON validity, App ID format, path rules).
- Android Assets Validator does the same for
assetlinks.json, including fingerprint and package checks.
Paste your domain in and it tells you exactly which check fails, rather than leaving you guessing at the OS's silence.
How Flinku handles this for you
If you use Flinku for deep linking, the AASA and assetlinks.json files are generated and served automatically for your subdomain and any custom domain, with the correct content type, no redirects, and a clean 200. You add your Associated Domains entitlement in Xcode (or your intent filter on Android) pointing at your Flinku domain, and the file-serving side is handled, including deferred deep linking for users who don't have the app installed yet.
That removes most of the causes on this list, since the file serving, content type, and .well-known routing are the parts most teams get wrong.
The short version
When Universal Links or App Links won't verify, check these in order:
- No redirects, clean
200on the.well-knownURL - Correct content type (
application/json), no extension on the iOS file - Valid JSON structure
- App ID and Team ID (iOS) or package name and release fingerprint (Android) match exactly
- Account for Apple's CDN cache delay when testing
- Path rules actually include the URL you're testing
- Test on a physical device, not a simulator
Most "deep links won't open my app" problems are one of these seven. Work down the list and you'll find it.