Universal links are one file at one exact path, and nothing tells you when it is wrong.
A universal link on iOS needs a file named apple-app-site-association, with no extension, served at https://yourdomain.com/.well-known/apple-app-site-association. An app link on Android needs a file named assetlinks.json, served at https://yourdomain.com/.well-known/assetlinks.json with content type application/json. Both have to come back over HTTPS and both have to come back without a redirect. Get a character wrong and the build still succeeds, the app still runs, and the link just quietly opens a web page. That silence is the whole problem, and the rest of this page is about breaking it. For what the feature is for in the first place, start with deep linking.
What follows is the exact path for each platform, the one serving rule that differs, how long each platform takes before it believes you, and the commands that print a verdict instead of a guess. Every figure and field name below comes from Apple's and Google's own documentation, read on 10 October 2026.
Start hereThe short version
Hosting the file takes ten minutes. Proving the device agrees is the job.
Nobody loses a day writing the JSON. They lose it because neither platform shouts when the file is unreachable, redirected, served with the wrong name, or signed with the wrong certificate. There is no build error and no runtime warning. The link opens the web page, which is exactly what it would do if you had never set any of this up.
So treat it as two tasks. Publish the file at the exact path, then run the checks that make each platform tell you what it thinks. On iOS that is curl, swcutil on a Mac and the Diagnostics screen in Developer settings. On Android it is one adb command that prints a verification state per host. Doing the second task is the difference between twenty minutes and a day, and it is the part most write-ups skip.
Which file goes where, and does either one need a content type?
Apple wants a file named apple-app-site-association with no extension at all, in your site's .well-known directory, so the URL reads https://yourdomain.com/.well-known/apple-app-site-association. Apple's associated domains documentation requires https with a valid certificate and no redirects. It describes the contents as JSON and does not state a required content type header. Naming the file apple-app-site-association.json is an easy way to get a 404 that looks like a mystery.
Google is stricter about serving and looser about naming. The file is assetlinks.json, published at https://domain.name/.well-known/assetlinks.json, and Google's guide to publishing it names three rules. It is served with content type application/json. It is reachable over an HTTPS connection whether or not your intent filters declare https as the data scheme. And it is reachable without any redirects, no 301 and no 302. So the answer to the content type question is different per platform, which is why a configuration copied from one to the other half works.
Hosts matter more than people expect. On iOS, a file at the root domain serves an entry of applinks:example.com and subdomains matched by the wildcard applinks:*.example.com. It does not serve a specific entry such as applinks:www.example.com, which needs its own file at https://www.example.com/.well-known/apple-app-site-association. Apple also notes that a domain in your entitlement cannot carry a port number. On Android, each unique host in your intent filters is queried separately, so every host serves its own copy.
One more thing about these files: they are public by design. The Apple file lists your application identifier prefix and your bundle identifiers, the Android file lists your package name and your certificate fingerprints. That is fine, it is how verification works, but it means the association file is not a place for anything private, and it is worth a look over your app security basics before you publish one.
Apple, TN3155 Debugging universal links, read 10 October 2026
How long does verification take, and what sets it off?
iOS stopped asking your server directly. Since macOS 11 and iOS 14, apps send these requests to an Apple managed content delivery network instead. Apple documents that the CDN requests the file for your domain within 24 hours, that a device downloads it from the CDN when your app is installed, and that devices then check for updates approximately once per week. There is no direct CDN invalidation option, so to pick up a newer file on a device you reinstall the app. Fixing the file and seeing the fix are two separate events, often days apart.
That is also why Apple provides a way around its own cache while you work. If your server is not reachable from the public internet, or you are changing the file faster than the CDN can keep up, you add a query string to the associated domain in your entitlement to turn on an alternate developer mode, which pulls the file straight from your domain. Useful during setup. Remember to take it out, because it is not how a shipped app behaves.
Android runs the check on the device. With android:autoVerify set to true on at least one intent filter, installing the app on Android 6.0 or higher starts verification, and Google says to wait at least 20 seconds for the asynchronous process to finish before you judge the result. Changes to a published file are slower. On Android 15 and higher the system re-verifies in the background and a change can take up to seven days to reach every device. On Android 14 and lower there is no periodic re-verification at all, so an update is typically only picked up when the app is installed or updated. Even a reinstall can hit a server side cache, which Google says is time based and usually clears within a few hours.
One behaviour from older releases is worth knowing if you still support them. On Android 11 and lower, the system makes your app the default handler only if it finds a matching file for every host in the manifest. One test host you forgot to remove can cost you every link in the app. This is also the moment to decide which links are allowed to carry anything sensitive, because a link that signs someone in belongs in the same conversation as authentication, not in a list of nice URLs.
How do you debug an iOS link that silently opens Safari?
Start with the response, not the JSON. Apple's technote on this tells you to log the HTTP response headers and the file contents with curl -v against the .well-known path. A 301 or 302 means your server is redirecting, which is not supported for this file. A 403 or 404 means the site denied access, and the usual cause is that the path is not publicly reachable from every IP address and every geography. Apple does not publish the addresses or ranges it fetches from, so there is nothing to allowlist. Your server also has to answer any user agent, which you can prove by sending a nonsense one with curl -A and checking you still get the file back.
Next, check the format. The current shape is an appIDs array plus a components array. The older shape is an appID string plus a paths array. Apple warns against mixing the two, because mixing them can produce behaviour you did not ask for. Then use swcutil, which is built into macOS. Run sudo swcutil dl with your domain to confirm the JSON downloads, then sudo swcutil verify with your domain, the downloaded file and a test URL. It prints that the pattern matched, or that the pattern was blocked, which is what you want to see for a path you deliberately excluded and the last thing you want to see for one you did not.
There is also an on-device check that almost nobody uses. Turn on Developer Mode, open Settings, go to Developer, scroll to the section labelled Universal Links, turn on Associated Domains Development, then open Diagnostics and type a full URL. The phone tells you whether that link is valid for an installed app. If you need to go deeper, take a sysdiagnose on a device with the app installed and open swcutil_show.txt inside it. Find your app identifier and read four lines. User Approval is the person's own choice. Site/Fmwk Approval is the one that matters: approved means Apple's CDN fetched your file and pattern matched it, while unspecified or denied means it could not. Last Checked and Next Check tell you when the CDN asked your site for the file and when it will ask again.
Two behaviours are not bugs, and recognising them saves hours. Typing a URL into the browser address bar never opens the app, because that is direct navigation. And a universal link that points at the same domain the person is already browsing is treated as more navigation, so a login button on your own site will not hand off to the app unless you move it to a different subdomain with its own association file. To test properly, paste the link into Notes and long press it: a correct setup offers both the app and the browser, and whichever you pick becomes that device's default for the domain. The same handoff is what happens when a link leaves your app through the share sheet and comes back in somewhere else.
Apple, TN3155 Debugging universal links, last revised 15 May 2025
What does Android actually tell you when an app link fails?
More than iOS does, and in one command. Run adb shell pm get-app-links with your package name and you get a domain verification state for every host. Only one value means success: verified. Google is blunt that any other state means verification could not be performed. The documented list is none, verified, approved, denied, migrated, restored, legacy_failure and system_configured, plus device-specific error codes of 1024 or higher. Read two of them carefully. none means the agent may not have finished yet, so wait and ask again. approved does not mean your file works: Google defines it as force-approved, usually by a shell command, so a state you set by hand while testing can look like a pass forever.
To test a fix, clear the slate first. adb shell pm set-app-links with your package resets the state to what it was before any defaults were chosen, and adb shell pm verify-app-links with the re-verify flag runs the check again. Give the verification agent a couple of minutes before you read the result. A second command, adb shell dumpsys package domain-preferred-apps, lists which app handles which domains, and a status of always for a verified app. Read it with care: Google notes that the hexadecimal number after the status does not indicate whether verification succeeded, and that a user who changed the link setting before verification finished can make a failure look like a pass.
The fingerprint is the failure you should expect. The file carries sha256_cert_fingerprints for the certificate that signs the build on the user's device. If Play App Signing is on, the fingerprint you get from keytool on your laptop is the upload key and usually will not match. The one you need is in Play Console under Release, Setup, App signing, which also hands you a ready made JSON snippet. Google's troubleshooting guide adds that the value has to be upper case, and that putting only the release signature in the file while testing a debug build fails the same silent way.
There are three ways to check without a device in your hand. The Statement List Generator and Tester tool validates an existing file. The Digital Asset Links API answers the same question over HTTP: ask digitalassetlinks.googleapis.com for statements:list with your site and the delegate_permission/common.handle_all_urls relation, and read what Google can see. And Play Console has a Deep links page under Grow for an uploaded app, which lists configuration errors and how to fix them. On Android 17 and higher there is also an am start flag called debug-link that prints which candidate apps, manifest filters and rules were evaluated for a given URL, which is the closest either platform comes to showing its work.
Android Developers, Verify App Links, read 10 October 2026
Pick the symptom
What to check next, per platform
Choose what you are seeing. Each answer is the next thing to look at on each platform, not a list of everything it could be.
Nothing picked yet. If you have not run the file through curl on both paths, start there, because a bad status code explains most of these.
The same job, two files that behave differently
| What you want to know | iOS, universal links | Android, app links | Same on both |
|---|---|---|---|
| File name and location | apple-app-site-association in /.well-known, no extension | assetlinks.json in /.well-known | No |
| Content type required | Not stated in Apple's documentation | application/json | No |
| Redirect allowed on the file | No | No | Yes |
| Who fetches the file | Apple's content delivery network | The device, through the verification agent | No |
| First check after install | Downloaded from the CDN at install | About 20 seconds after install | No |
| A changed file reaches devices | About once a week, or on reinstall | Up to seven days on Android 15 and higher | No |
| A command that prints a verdict | sudo swcutil verify | adb shell pm get-app-links | No |
| A web console that shows status | No | Play Console, under Grow, Deep links | No |
Where this lands if a builder generated your app
Half of this work is in the app and half is on your web server, and no tool can do the second half for you. The entitlement and the intent filter live in the project. The two files live on a domain only you control. That split is worth checking before you pick a builder, because a platform that will not let you edit the iOS entitlement or the Android manifest cannot do universal links at all.
Newly is an AI app builder. You describe an app in plain English and it writes a real React Native and Expo project you own, with two way GitHub sync, so the project configuration that declares an associated domain or an intent filter is yours to read and change like any other code. It uploads iOS builds to TestFlight, publishes to Google Play internal testing and can build a standalone Android APK. It is $25 a month and there is no free tier.
Two honest limits. Newly does not submit to the App Store for you, because Apple requires the owner of the content to submit, so the final step is yours. And nothing in a builder puts a file on your domain or tells you that Apple's CDN could not read it. Plan on publishing the two files yourself, then running the checks above once against a real install on each platform. That is an afternoon, and it is the only version of this that ends with you knowing rather than hoping.
Questions people ask about universal links and app links
At https://yourdomain.com/.well-known/apple-app-site-association. Apple's documentation says to create a file named apple-app-site-association without an extension, place it in your site's .well-known directory, and host it using https with a valid certificate and with no redirects. A file at the root domain covers the root domain and any subdomain matched by a wildcard entry. It does not cover a specific subdomain entry such as www, which needs its own file at its own path.
Publish the file, then make the device say so
Put both files at their exact paths, install the app once on each platform, and run the two commands that print a verdict. You will know in twenty minutes instead of finding out from a user who tapped a link and landed on a web page.
Start building