apple-app-site-association and assetlinks.json: a setup guide
The two files that let your domain open your app. What goes in each, the exact formats, where and how to host them, and how to check them before you ship.
Before an iPhone opens your app from an https:// link, it needs proof that the domain and the app belong together. Android needs the same. That proof is two small JSON files on your domain: apple-app-site-association for iOS Universal Links, and assetlinks.json for Android App Links.
This guide covers setting them up: what goes in each file, the exact formats, where and how to host them, and how to check them before you ship. If you already have the files and links still open the browser, go to our troubleshooting checklists for iOS and Android.
What the two files do
Both files work the same way. Your app declares the domains it wants to handle. When the app is installed, the operating system fetches a file from each of those domains. If the file names your app, links to that domain open the app instead of the browser.
| iOS | Android | |
|---|---|---|
| File | apple-app-site-association (no extension) |
assetlinks.json |
| URL | https://<domain>/.well-known/apple-app-site-association |
https://<domain>/.well-known/assetlinks.json |
| Identifies the app by | Team ID + bundle ID | Package name + SHA-256 of the signing certificate |
| App side | Associated Domains entitlement: applinks:<domain> |
Intent filter with android:autoVerify="true" |
| Who fetches it | Apple's CDN, then devices read from the CDN | Each device, on install (and periodically on newer versions) |
Collect what you need first
- Apple Team ID. The 10-character ID under Membership details on developer.apple.com. It is not your App Store app ID.
- Every bundle ID that should open the links, including staging or
.devvariants you test with. - Android package name for each variant.
- The SHA-256 fingerprint of every key that signs a build you install: your debug key, your upload or release key, and, if you use Play App Signing, the app signing key from Play Console. With Play App Signing, Google re-signs the app, so the fingerprint of the keystore on your laptop won't match what users install from Play.
# Debug key
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
# All variants of a Gradle project
./gradlew signingReport
The apple-app-site-association format
Apple's Supporting associated domains page describes the current format, which uses appIDs and components:
{
"applinks": {
"details": [
{
"appIDs": ["ABCDE12345.com.example.shop", "ABCDE12345.com.example.shop.staging"],
"components": [
{ "/": "/account/*", "exclude": true, "comment": "Keep account pages in the browser" },
{ "/": "/product/*", "comment": "Product pages" },
{ "/": "/help/*", "?": { "article": "?*" }, "comment": "Help pages with an article parameter" },
{ "/": "/*" }
]
}
]
},
"webcredentials": {
"apps": ["ABCDE12345.com.example.shop"]
}
}
What each part means:
appIDs:<Team ID>.<bundle ID>for each app that may open these links.components: an ordered list of URL patterns. The first match wins, so putexcluderules before a catch-all/*.webcredentials(optional): lets iOS offer saved passwords for this site in your app. It isn't needed for links.
The keys a component can use, from Apple's components reference:
| Key | Matches | Default |
|---|---|---|
/ |
The URL path | * (everything) |
? |
The query: a pattern, or a dictionary of parameter names to patterns | * |
# |
The fragment | * |
exclude |
If true, a match stops here and the link opens in the browser | false |
caseSensitive |
Whether matching is case-sensitive | true |
percentEncoded |
Whether the URL is treated as percent-encoded | true |
comment |
Ignored. Use it to explain the rule. |
Wildcards: * matches zero or more characters, ? matches exactly one, and ?* matches one or more. A component only matches when all of its keys match.
Notes from experience:
/product/*doesn't match/products/42. Write patterns from real URLs, then test them (below).- Don't mix formats in one entry. The older
appID+pathsformat still exists for very old iOS versions; Apple advises against mixing it withcomponentsin the same entry. - Keep it small. Apple's CDN ignores files over 128 KB. If you exclude thousands of individual paths, rethink the URL structure instead.
The assetlinks.json format
From Android's Digital Asset Links guide:
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.shop",
"sha256_cert_fingerprints": [
"14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5",
"FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C"
]
}
}
]
(The fingerprints are examples. Use your own.)
- The file is a JSON array. Add one statement per app if several apps share the domain.
sha256_cert_fingerprintsis a list, so debug, upload and Play signing keys can all go in one statement.- Fingerprints are uppercase hex pairs separated by colons. Copying the snippet Play Console shows on the App signing page avoids typos.
- Unlike the iOS file,
assetlinks.jsondoesn't decide which paths open the app. That's the intent filter in your manifest:android:host, pluspathPrefixorpathPatternif you want to limit it.
Android 15: Dynamic App Links
On Android 15 and later, assetlinks.json can also carry path rules, which the device merges with your manifest. Android's guide shows them under relation_extensions:
"relation_extensions": {
"delegate_permission/common.handle_all_urls": {
"dynamic_app_link_components": [
{ "/": "/account/*", "exclude": true },
{ "/": "*" }
]
}
}
These rules can only narrow what the manifest declares, never widen it. Devices on Android 14 and earlier ignore them, and if any field is malformed, Android discards the dynamic rules and uses the manifest alone.
Where and how to host them
Both platforms are strict, and they fail silently. The rules, from Apple's and Android's docs:
- Exact path under
/.well-known/. The iOS file has no.jsonextension. - HTTPS with a valid certificate, returning HTTP 200.
- No redirects. Not
httptohttps, notexample.comtowww.example.com. - Content type
application/json. - On every host your app lists.
example.com,www.example.comandlinks.example.comeach need their own copy. - Reachable by crawlers. Bot protection or geo-blocking can stop Apple's CDN or Google's verifier while your browser works fine.
Nginx
location = /.well-known/apple-app-site-association {
default_type application/json;
try_files $uri =404;
}
location = /.well-known/assetlinks.json {
default_type application/json;
try_files $uri =404;
}
try_files … =404 matters for single-page apps: without it, a fallback rule can return your index.html with a 200, and the "file" is really HTML.
Next.js
Put both files in public/.well-known/, then set the content type for the one without an extension in next.config.js:
module.exports = {
async headers() {
return [
{
source: "/.well-known/apple-app-site-association",
headers: [{ key: "Content-Type", value: "application/json" }],
},
];
},
};
After you deploy
Check the raw responses:
curl -sI https://links.example.com/.well-known/apple-app-site-association
curl -sI https://links.example.com/.well-known/assetlinks.json
You want HTTP/2 200 and content-type: application/json, with no location: header.
How long changes take
- iOS. Since iOS 14, devices read the file from Apple's CDN, not your server. Apple says the CDN requests your file within 24 hours, and devices check for updates about once a week after the app is installed. See what the CDN has with
curl https://app-site-association.cdn-apple.com/a/v1/links.example.com. During development,applinks:links.example.com?mode=developerwith Associated Domains Development turned on skips the CDN. - Android. Verification runs when the app is installed or updated. Per Android's docs, Android 15 and later may also pick up changes on their own, which can take up to seven days. During testing,
adb shell pm verify-app-links --re-verify com.example.shopchecks again immediately.
Check both files with one tool
Our free Universal Link and App Link Validator fetches both files the way phones do, with redirects turned off. For the iOS file it checks HTTPS, status, content type, size, JSON structure and app ID format, tests a path you give it against your components, and compares your server's file with Apple's CDN copy. For Android it checks the JSON, package names and fingerprint format, and asks Google's Digital Asset Links API whether it can read your statements.
On a Mac, Apple's own tool tests a path against a downloaded file:
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
Or let CodeOnAir host them
If links are all you need the domain for, you don't have to host these files yourself. In CodeOnAir AppLinks you enter your Team ID, bundle ID, package name and fingerprints (up to 20, so every signing key fits), and CodeOnAir builds and serves both files on every link domain: your free yourapp.codeonair.com subdomain, or your own domain with the custom domain add-on. When you mark a link to open in the browser on iOS, it's added to the exclusions in the file for you.
The dashboard's App setup box shows the Associated Domains entry and intent filter with your domains filled in, and its Check button reads the files the way phones do. Setup steps are in the AppLink quick guide. Building in React Native or Expo? See our guides to React Native deep linking and Expo deep linking.
FAQ
Does apple-app-site-association need a .json extension?
No. Serve it at /.well-known/apple-app-site-association with no extension, with an application/json content type, over HTTPS and without redirects.
Can one domain serve several apps?
Yes. On iOS, list several app IDs in appIDs, or add more details entries with different components. On Android, add one statement per package to the assetlinks.json array.
Do I need the files on both www and the root domain?
Only if your app lists both. Every host in the Associated Domains entitlement or an autoVerify intent filter is checked separately at its own /.well-known/ path.
Which SHA-256 fingerprint goes in assetlinks.json?
The fingerprint of the certificate that signed the build on the device. For Play Store installs that's the app signing key in Play Console; for local builds it's your debug or upload key. List all of them.
How do I exclude some paths from opening the app?
On iOS, add a component with "exclude": true above your catch-all rule. On Android, narrow the intent filter with pathPrefix or pathPattern, or on Android 15 and later use dynamic_app_link_components with an exclude rule.
Set up links without the hosting
CodeOnAir AppLinks hosts both files, gives you a link domain, and adds deferred deep linking and analytics. The Free plan includes AppLinks on one URL prefix, and paid plans add more (pricing); sign-ups are currently by invitation, so request access.
Formats checked on 25 October 2026 against Apple's "Supporting associated domains" and applinks components reference, and Android's "Configure the Digital Asset Links file" guide.