shipcheck

Universal links: the setup that fails without an error

Updated September 7, 2026 · published by Baker Ventures LLC · sources cited inline

Nothing errors when this is wrong. The link just opens the website, and you cannot tell why.

Four causes cover nearly all of it, in the order to check them:

1. The association file is not served correctly. It lives at the well-known path, over HTTPS, served as JSON, with no redirect and no extension. Serving it as HTML, or letting a framework redirect /.well-known/*, is the most common single cause.

2. The app config does not declare the domain. Associated domains on iOS, intent filters on Android. In Expo this belongs in app config so prebuild writes it, not in the generated native project, which is regenerated.

3. The app was verified while the file was wrong. Verification happens around install, so a device that installed the app before you fixed the file can keep the broken state. "It works on my phone" is not evidence here; reinstall after every fix.

4. The link was opened somewhere that does not follow universal links. Some in-app browsers and some contexts deliberately do not, and that is expected behaviour rather than a bug in your setup.

On Android the extra failure is the signing fingerprint in assetlinks.json, particularly when Play App Signing re-signs your bundle and the certificate you published is not the one that ships.

Checking the files from outside

Do this from a machine that is not yours, because a cached or local answer is not the answer:

# iOS: must be JSON, 200, no redirect
curl -sIL https://example.com/.well-known/apple-app-site-association | head -20
curl -s https://example.com/.well-known/apple-app-site-association | head -40

# Android
curl -s https://example.com/.well-known/assetlinks.json | head -40

# the things to confirm in the headers
# - HTTP/2 200, not 301 or 302
# - content-type: application/json
# - no authentication, no geo-blocking, no bot protection challenge

Bot protection is the modern version of this problem. A challenge page served to a non-browser client returns something that is not your JSON, and the platform sees exactly what curl sees.

The Expo specifics

// app.json
"ios": { "associatedDomains": ["applinks:example.com"] },
"android": {
  "intentFilters": [{
    "action": "VIEW",
    "autoVerify": true,
    "data": [{ "scheme": "https", "host": "example.com" }],
    "category": ["BROWSABLE", "DEFAULT"]
  }]
}

Then verify it survived generation, because a config that is correct in app.json and absent from the build is the second most common cause after the file:

npx expo prebuild --clean
grep -A3 'associated-domains' ios/*/*.entitlements
grep -A6 'intent-filter' android/app/src/main/AndroidManifest.xml

Editing the generated ios/ or android/ directly works locally and disappears at the next build. The EAS submission checklist. · Auditing what your config plugins added.

Testing it properly

1. Fix everything. Confirm the files with curl from outside.
2. Delete the app from the test device.
3. Reinstall. Verification happens around install.
4. Open the link from Notes or Messages, not from a browser
   address bar, which does not always behave the same way.
5. Test a path that should open the app and one that should not.
6. Test with the app force-quit, and with it backgrounded.

Step two is the one people skip, and it is the reason this gets debugged for a day.

The Android fingerprint

assetlinks.json carries the SHA-256 fingerprint of the signing certificate. With Play App Signing, Google re-signs your bundle, so the fingerprint that matters is the one from the Play Console's app signing page, not the one from your upload key.

Publishing the upload key's fingerprint is a silent failure that looks exactly like everything else being wrong. Check the Console.

Why this matters beyond convenience

A broken link is a broken feature, and if onboarding, a password reset, an invite flow or your review notes depend on a link opening the app, a reviewer experiences an app that does not work. That is reported as Guideline 2.1 completeness rather than as a linking problem, which is why the guideline number does not find you the answer.

And it is the kind of thing that works in development and fails in production, because the domain, the certificate and the hosting are all different there. Test against the production domain with the production build. Shipping your first update.

shipcheck reports whether the domains declared in your app config match the association files your site actually serves, and flags a signing fingerprint mismatch. What it checks. · When a dependency blocks your release.

About shipcheck

shipcheck is a pre-submission checker from Baker Ventures LLC that runs inside Claude Code. It reads your React Native or Expo project, finds the things that get apps rejected or blocked at upload, cites the exact App Store Review Guideline or Google Play policy clause, and names the file and line to fix.

It exists because React Native and Expo apps fail for reasons that are invisible in the code you wrote: capability arriving through a dependency, purpose strings added by a config plugin, a privacy manifest missing from an SDK you never chose directly, and a generated ios/ directory that discards your edits on the next prebuild. shipcheck checks what the binary and the config actually declare, not what you intended. The rejection references on this site are free, need no account, and link to the primary Apple or Google document for every claim.

Questions and answers

Almost always one of four things: the association file is not served correctly, the app config does not declare the domain, the app was never verified because the file was wrong at install time, or the link was opened in a context that deliberately does not follow universal links.

What is the apple-app-site-association file?

A JSON file served from your domain that tells iOS which app is allowed to handle which paths. It must be served over HTTPS from the well-known path, with a JSON content type, and without a redirect.

Does the association file need a .json extension?

No, and it must be served as JSON. The file at the well-known path has no extension, and serving it as HTML or letting a framework redirect it is one of the most common causes of silent failure.

Why does it work for me and not for a tester?

Verification happens around install, so a device that installed the app while the file was wrong can keep the wrong state. Reinstalling after the file is correct is part of testing this, and it is why "it works on my phone" is not evidence.

The equivalent is assetlinks.json with your signing certificate fingerprint, and the most common failure is a fingerprint from the wrong signing key, particularly when Play App Signing re-signs your bundle.

Does this affect App Review?

Only indirectly. A broken link is a broken feature, and if your review notes or your onboarding depend on a link opening the app, a reviewer will experience it as an app that does not work.

Cite this pageshipcheck. “Universal links: the setup that fails without an error.” Baker Ventures LLC, September 7, 2026. https://shipcheck.bakerventuresstudio.com/guides/universal-links-and-deep-links/