Skip to content

Launch7 min read

Deep links, Universal Links and App Links: making campaign links open the right screen

By Published

Short answerA campaign link should be a normal https address that your app is verified to open: a Universal Link on iPhone and an Android App Link on Android. Verification relies on two files on your website, apple-app-site-association and assetlinks.json, served over HTTPS with no redirects. The same address needs a working web page for people without the app, and every link should be tapped on real phones before the campaign spends money.

A deep link opens an app on a specific screen instead of the home screen. Flutter's own docs use the example of a sneaker ad that opens a shopping app on that product's page. That is the whole job of a campaign link.

There are three ways to build one. A custom scheme looks like myapp://offer/42 and only works if the app is installed. Android's docs say plain deep links are subject to the system disambiguation dialog, the box that asks which app should open the link. The other two use ordinary web addresses. On Android they are App Links, which Android verifies against your website and then opens directly in your app with no dialog, on Android 6 and later on devices with Google services. On iPhone they are Universal Links, which use only http or https.

I would not put a custom scheme in an ad, an email or a QR code. A web address that the app is verified to open does both jobs: Android's docs say users who do not have the app go to your website instead. So the rule is one URL per piece of content, and a real web page behind every URL you print.

The two files your website has to serve

Verification is the part owners rarely hear about. Each platform checks a file on your domain that names your app. If the file is missing, wrong or behind a redirect, the link opens in the browser and the campaign still looks fine from a laptop.

What to checkiPhone: Universal LinksAndroid: App Links
Fileapple-app-site-association, with no file extensionassetlinks.json
Addresshttps://your-domain/.well-known/apple-app-site-associationhttps://your-domain/.well-known/assetlinks.json
Serving rulesHTTPS with a valid certificate and no redirectsHTTPS, content-type application/json, no 301 or 302 redirects
How it names your appappIDs in the form TeamID.BundleID, plus components listing the paths that open the apppackage_name plus the SHA-256 fingerprint of the app's signing certificate
SubdomainsEach subdomain needs its own entitlement entry and its own fileThe file must be published on each host the app handles
When changes reach phonesApple's CDN requests the file within 24 hours; devices check roughly once a week after installAndroid 15 and later re-verify in the background, up to seven days; Android 14 and lower usually only on install or update

None of these failures show an error. The link just opens a web page, or opens the app on the wrong screen.

  • The www question. www.example.com and example.com are different hosts. Apple says each subdomain needs its own file, and Android needs the file on every host. Print one form and make sure both are covered.
  • The wrong fingerprint. Android's docs warn that with Play App Signing, the fingerprint from your local keystore usually will not match the one on users' phones. The correct snippet is in Play Console under the App signing page.
  • A path the file does not list. On iPhone, the components in the file decide which paths open the app. A new campaign path such as /ramadan-offer may need adding, and Apple's CDN then needs time to pick it up.
  • The app opens, but on the home screen. Verification only gets the user into the app. The app still has to read the path and show the right screen, both when it was closed and when it was already running.
  • A third-party link plugin fighting Flutter. Flutter's docs say plugins such as app_links conflict with Flutter's default handler, and you turn the default off if you use one. Pick one approach.
  • Old Firebase Dynamic Links. Firebase's FAQ says the service shut down on August 25, 2025, and clicked links now return a 404. Any page.link address still on a flyer, QR code or old email is dead.
  • Links that do too much. Apple calls universal links a potential attack vector and says to validate every URL parameter and not let a link delete content or reach sensitive information. A link can open an order. It should not cancel one.

Flutter's setup guides for both platforms use the go_router package, with a route for each screen a link can reach. That is how I would build it: list the campaign URLs first, then make each one a route, so /offer/42 maps to the offer screen with 42 as its parameter. Flutter's deep linking page explains that with the Router approach, a new link that arrives while the app is running replaces the current stack of pages. Test that case on purpose.

From Flutter 3.27, the default deep link handler is on without extra setup, per the same guides. Older projects need a flag added on each platform.

Android 15 adds Dynamic App Links. You keep the manifest broad, scheme and domain only, and refine paths and exclusions in assetlinks.json without shipping an app update. Google's own example is a seasonal sale path that you add for the campaign and remove when it ends. Two limits: the server rules cannot widen what the manifest declares, and phones on Android 14 or lower ignore them and use the manifest alone. I would still plan campaign paths before the release, not after.

Run this with the exact URLs the campaign will use, including the tracking parameters your ad platform adds. One iPhone and one Android phone, store builds where possible.

  • Open both files in a browser on the live domain. Both should load directly, with no redirect.
  • On Android, run adb shell pm get-app-links with your package name. Every domain should say verified. Android's docs list what other states mean.
  • On iPhone, type the link into the Notes app and tap it, which is the test Flutter's guide suggests. On a Simulator, xcrun simctl openurl booted with the URL does the same.
  • On Android, tap the link from another app, such as a Google Doc. Flutter's guide points out that an adb launch alone does not prove the website files are right.
  • Tap each link with the app closed, then again with the app open on a different screen.
  • Tap each link with the user logged out. Decide what they see: the login screen, then the offer.
  • Uninstall the app and tap again. The web page should load and make sense on its own.
  • Try a broken version: a wrong product ID, a missing parameter. The app should show a sensible screen, not crash.
  • Scan every printed QR code and check none still points at a page.link address.
  • Repeat the full list after any change to the files, allowing for Apple's 24-hour CDN window.

Deep link routing is not a line item in my package scope, so I would name it in the written scope before work starts: the URL patterns, the screen each one opens, and who hosts the two files. Launch ($999) covers up to 10 screens, Growth ($1,699) up to 25 with its own Node.js backend, and Scale ($2,500) up to 50 with an admin dashboard and push notifications where supported. Push is a separate topic, but a notification tap and a campaign link should land on the same routes.

If you want attribution, meaning which ad led to which install, that comes from a third-party provider, and third-party fees, hosting and ongoing maintenance are not included in any package. Send me the list of links your next campaign will use, and I will tell you which ones would open the wrong screen today.

Questions

What replaced Firebase Dynamic Links?
Firebase's FAQ says the service shut down on August 25, 2025. For links that open an installed app, it points to Android App Links and iOS Universal Links. For full feature parity, such as keeping the link through an install, it lists third-party deep linking providers, which it notes Google has not vetted.
Why does my link open the browser instead of the app on Android?
Usually verification failed. Run adb shell pm get-app-links with your package name. If a domain is not verified, check that assetlinks.json loads over HTTPS with no redirect and carries the Play App Signing fingerprint, not your local one.
How long after I change the files will links work?
On iPhone, Apple says its CDN requests the file within 24 hours and devices check about once a week after install. On Android 15 and later, Google says changes can take up to seven days to reach all devices.

Sources

Platform rules and fees in this article were checked against these official pages on 10 November 2026. They change, so confirm before you rely on them.

Sannan Malik is a software architect and product builder with 8+ years in software, CEO of Alpha Notic FZC and a Certified Scrum Product Owner. He builds mobile apps and business software for companies in the UAE, UK and US.

Let's build it

Got an idea? Let's talk.

The fastest way to reach me is WhatsApp. Tell me what you're building and I'll reply with how I'd approach it, which package fits, or what a custom scope would look like.

Message me on WhatsApp