Universal Links
Universal Links not working? A debugging checklist for iOS
When Universal Links silently open Safari instead of your app, the cause is almost always one of these ten gotchas. A systematic checklist to find which one is biting you.
The most frustrating thing about iOS Universal Links is that when they break, iOS gives you no error message. The link just opens Safari. Your app doesn’t launch. There’s nothing in the console to tell you why.
After helping a lot of indie developers debug this, the cause is almost always one of about ten things. This is the systematic checklist to walk through them, in order of how often they’re the culprit.
Run through it top to bottom. Stop when something fails — that’s almost certainly your problem.
0. First: confirm you’re testing correctly
Before you assume Universal Links are broken, make sure you’re not just testing them wrong:
- Don’t paste the link into Safari’s address bar. Safari treats those as in-Safari navigations and won’t trigger Universal Links. Send yourself the link via Notes, Messages, or Mail. Long-press it.
- You should see “Open in <App Name>“ as the first option in the long-press menu. If you only see “Open in Safari”, Universal Links aren’t activating.
- Test on a real device, not the simulator. Universal Links don’t reliably work in iOS Simulator — Apple’s
swcddaemon behaves differently there.
1. Is the AASA file reachable at the canonical URL?
curl -I https://your-domain.com/.well-known/apple-app-site-association
You should see HTTP/2 200. If you see 404, your file is missing or at the wrong path. If you see 301 or 302, you have a redirect — see #2.
Also test the legacy root path, which third-party validators check:
curl -I https://your-domain.com/apple-app-site-association
Modern iOS uses /.well-known/, but some validators flag a missing root-path file as a setup error. Easiest fix: serve both.
2. Are there any redirects in the way?
Apple’s swcd will not follow 301 or 302 responses for the AASA file. The file must be at the canonical URL with a direct 200.
Common offenders:
- HTTP-to-HTTPS redirects. Apple’s CDN always requests HTTPS, so this rarely matters — but if you’re testing in dev, make sure you’re hitting HTTPS directly.
- Trailing-slash redirects. Some web servers redirect
/folder→/folder/. Make sure the AASA URL doesn’t get redirected. - Apex-to-www redirects (or the reverse). If your DNS redirects
myapp.com→www.myapp.com, the AASA file atmyapp.com/.well-known/apple-app-site-associationwill fail.
The fix: serve the file directly from the canonical URL, no redirect chain.
3. Is the Content-Type header exactly application/json?
curl -I https://your-domain.com/.well-known/apple-app-site-association | grep -i content-type
Must say content-type: application/json. Common wrong values:
text/plain— silent failureapplication/octet-stream— silent failure (this is what GitHub Pages serves by default)application/json; charset=utf-8— Apple is OK with the charset suffix, but bareapplication/jsonis what the docs specify
4. Does the file have a .json extension?
It must not. The filename is literally apple-app-site-association with no extension. If your editor or build process appended .json, Apple’s swcd won’t find it at the expected path.
curl -I https://your-domain.com/.well-known/apple-app-site-association.json
If that returns 200 but the no-extension URL returns 404, that’s your problem.
5. Is the JSON actually valid?
curl -s https://your-domain.com/.well-known/apple-app-site-association | jq .
If jq errors, your JSON is broken. Common issues:
- Trailing commas in arrays or objects (legal in JS, illegal in JSON)
- Single quotes instead of double quotes
- Unquoted keys
- A BOM (byte-order mark) at the start of the file from Windows-edited files
6. Does the App ID format match exactly?
The appIDs array entries must be in TEAMID.bundleid format:
"appIDs": ["ABCDE12345.com.yourcompany.yourapp"]
- The Team ID is exactly 10 alphanumeric characters (uppercase letters and numbers).
- The bundle ID must match what’s in your Xcode project’s
Bundle Identifierfield exactly. Case-sensitive. - A common typo: using a lowercase Team ID. Apple’s are always uppercase.
You can find your Team ID in Apple Developer → Membership.
7. Does the Xcode Associated Domains entitlement match?
In Xcode, select your app target → Signing & Capabilities → Associated Domains. You should see:
applinks:your-domain.com
- No
https://orhttp://prefix. - No trailing slash.
- For subdomains, each one needs its own entry:
applinks:myapp.comandapplinks:www.myapp.comare different. - If you removed the capability and re-added it, delete the app from your device and reinstall — iOS caches the entitlement.
8. Is Apple’s CDN serving a stale version?
Starting in iOS 14, devices don’t fetch the AASA file directly. Apple’s CDN does it on their behalf, then devices pull from there. The CDN refreshes every 24–48 hours, so freshly-deployed AASA changes can take a while to propagate.
To see what the CDN currently has for your domain, hit:
https://app-site-association.cdn-apple.com/a/v1/your-domain.com
If this shows your old file, the CDN hasn’t refreshed yet. To bypass it during development, change your Xcode Associated Domains entitlement from:
applinks:your-domain.com
to:
applinks:your-domain.com?mode=developer
That tells iOS to skip the CDN and fetch the file directly from your origin. Remove the ?mode=developer flag before submitting to the App Store — it’s a development-only mode.
9. Did iOS actually re-fetch the AASA after your change?
iOS does not re-scrape the AASA file on app updates from the App Store. To force a refresh during development, delete and reinstall the app. (Yes, this is a real Apple-acknowledged limitation.) You can also reboot the device, which clears the swcd cache.
10. Is something blocking Apple’s AASA-Bot user agent?
Apple’s CDN identifies itself as AASA-Bot/1.0.0 when fetching your file. Some bot-protection layers will block it:
- Cloudflare with “I’m Under Attack” mode or aggressive bot challenges. Allow
AASA-Bot/1.0.0explicitly, or whitelist the User-Agent string. - Cloudflare’s “Always Use HTTPS” setting can sometimes interfere.
- AWS WAF with default rules.
- Custom IP allowlists that don’t include Apple’s CDN ranges.
Check your origin logs for AASA-Bot requests. If you don’t see them, something’s blocking the bot before it reaches your app.
11. Are your URL paths actually matching?
If swcd fetches the file successfully and your app entitlement is correct, but Universal Links still don’t open the app, the path patterns in your AASA file probably don’t match the URL you’re testing.
"components": [
{ "/": "/products/*" }
]
This matches /products/123 but not /products/ (no character after the trailing slash to match the *). Common pitfalls:
*doesn’t match an empty string. Use/products/**or include/products/separately.- Patterns are case-sensitive.
/Products/won’t match/products/*. - Query strings and fragments (
?utm_source=x,#section) are ignored when matching.
For wide-open testing, use "/": "*" (matches every path) to confirm the rest of your config works, then narrow down to specific patterns.
12. The “I just changed it and it stopped working” case
If everything was fine yesterday and stopped working today, scan your recent changes for:
- A web server config change (new redirect, new Cache-Control rule, new bot challenge)
- A DNS change (new CNAME, new Cloudflare proxy enabled)
- An SSL cert that expired or rotated to a different issuer
- A Cloudflare “Always Use HTTPS” toggle
- An app deploy that changed your build’s bundle ID
- A new web framework (Next.js / Nuxt / etc.) that started routing the well-known path through application code instead of serving it as a static file
This last one is sneaky: many web frameworks intercept all paths by default. They’ll serve a 404 page (with Content-Type: text/html) for /.well-known/apple-app-site-association instead of letting your static file be found.
A condensed diagnostic flow
If you want to walk through it fast:
# 1. File reachable, 200, no redirect
curl -I https://your-domain.com/.well-known/apple-app-site-association
# 2. Correct content-type
curl -I https://your-domain.com/.well-known/apple-app-site-association | grep -i content-type
# 3. Valid JSON, expected appIDs
curl -s https://your-domain.com/.well-known/apple-app-site-association | jq .
# 4. CDN sees the same content
curl -s https://app-site-association.cdn-apple.com/a/v1/your-domain.com | jq .
# 5. Branch's validator runs through everything
open https://branch.io/resources/aasa-validator/
If all five pass and Universal Links still don’t work, it’s almost certainly the Xcode entitlement (#7) or a stale device cache (#9 — delete and reinstall).
How OrbitKit handles all of this
When you host your AASA file with OrbitKit, every gotcha in this checklist is handled automatically:
- Served at both
/.well-known/apple-app-site-associationand the legacy root path Content-Type: application/jsonenforced- Zero redirects on the well-known path
- HTTPS via auto-provisioned SSL on your custom domain
- No Cloudflare or bot-challenge layer in front
- 128 KB size guard at config-save time
- File generated from a typed config (no JSON syntax errors)
- App ID format validated client-side and server-side
It’s $5/month per app and works on the first try. If you’re stuck on debugging and just want it to work, give it a try — there’s no credit card required to set up your first app.
Further reading
- Apple — TN3155: Debugging universal links (the official troubleshooting note)
- Apple — Supporting Universal Links in Your App
- Branch — AASA Validator (free third-party validator)
- Our AASA file generator guide
- Universal Links without a web server
- OrbitKit Universal Links docs