![]()
Universal Links and App Links for Streaming Apps: Setup Guide
A viewer taps "Watch episode 4" in your newsletter. On one phone, your app opens on episode 4. On another, the episode page loads in the browser, asks them to sign in, and offers a web player they never use. Same link, same app installed, two very different outcomes.
The difference is almost always Universal Links on iOS or Android App Links on Android, and one of a handful of small configuration mistakes. This guide covers how both systems work, the exact files and settings a streaming app needs, how to design URLs for a catalog of shows and episodes, and how to debug links that open the browser instead of the app.
How Universal Links and App Links work
Universal Links (iOS) and Android App Links are standard HTTPS links that the operating system opens in your app instead of the browser, because your domain has published a file proving it trusts that app. If the app is not installed, the same URL simply opens your website.
That last part is why streaming apps should prefer them over custom URL schemes such as myott://show/123. A custom scheme fails outright when the app is missing, and any app can claim the same scheme. An HTTPS link always has a working fallback, and only an app your domain has verified can claim it.
| iOS Universal Links | Android App Links | |
|---|---|---|
| Domain file | apple-app-site-association (AASA) | assetlinks.json |
| Location | https://yourdomain/.well-known/ | https://yourdomain/.well-known/ |
| App-side setting | Associated Domains entitlement: applinks:yourdomain | Intent filter with android:autoVerify="true" |
| App identity in file | Team ID + bundle ID | Package name + SHA-256 signing certificate fingerprint |
| Path control | components rules in the AASA file | Intent filter path, pathPrefix, or pathPattern |
| When verified | Fetched via Apple's CDN on install or update | Checked at install by the domain verification agent |
Neither system handles users who do not have the app yet. For those, a deferred deep link carries the destination through the app store, which we cover in how deferred deep linking cuts OTT CAC.
Design URLs your catalog can live with
Get the URL structure right before writing any configuration. Every share button, email, push notification, and ad will use it for years.
Good streaming URLs are:
- Built on stable content IDs.
/watch/s/48213/e/7survives retitling and localisation./watch/the-night-shift-season-2-episode-7breaks when marketing renames the show. - Readable where it helps. You can add a slug after the ID, such as
/show/48213/the-night-shift, as long as routing ignores the slug. - The same on web and app. If
/show/48213is a valid web page, the app link and the web fallback stay in sync automatically. - Explicit about what stays on the web. Account settings, billing, help pages, and password resets often work better in the browser.
A typical path plan for an OTT app looks like this:
| Path | Opens | Notes |
|---|---|---|
/show/{id} | Series page | Main target for ads and shares |
/watch/{id} | Player for a film or episode | Check entitlement before starting playback |
/live/{eventId} | Live event or channel | Fall back to the event page if it has ended |
/collection/{id} | Curated row or genre | Useful for seasonal campaigns |
/account/*, /billing/* | Website | Excluded from app links |
iOS setup: the AASA file and Associated Domains
The AASA file tells iOS which app may open which paths on your domain. For a streaming app it might look like this:
{
"applinks": {
"details": [
{
"appIDs": ["ABCDE12345.com.example.ott"],
"components": [
{ "/": "/account/*", "exclude": true },
{ "/": "/billing/*", "exclude": true },
{ "/": "/show/*" },
{ "/": "/watch/*" },
{ "/": "/live/*" },
{ "/": "/collection/*" }
]
}
]
}
}
Rules are evaluated in order and the first match wins, so exclusions must come before broader rules. A catch-all "/": "*" placed at the top silently shadows every exclusion below it.
Host the file at https://yourdomain/.well-known/apple-app-site-association with no file extension, served over HTTPS as JSON with a direct 200 response and no redirects. Then add applinks:yourdomain to the app's Associated Domains entitlement.
iOS does not download the file live from your server each time. Apple's CDN fetches and caches it, and the device picks it up when the app is installed or updated, so a fix can take time to reach users. Deeplinkly's free AASA and assetlinks.json generator builds both files from your app details, and its Universal Link tester shows which of your URLs will open the app and which rule decided it, without building the app.
Android setup: assetlinks.json and verified intent filters
On Android, the domain publishes assetlinks.json and the app declares which hosts and paths it handles.
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.ott",
"sha256_cert_fingerprints": ["AA:BB:CC:...:FF"]
}
}
]
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="watch.example.com" />
<data android:pathPrefix="/show/" />
<data android:pathPrefix="/watch/" />
<data android:pathPrefix="/live/" />
</intent-filter>
Three details cause most Android failures:
- The wrong fingerprint. With Play App Signing, Google re-signs your app, so the fingerprint must come from the Play Console, not your local keystore. Add your debug fingerprint as well if you test debug builds.
- A custom scheme in the same filter.
autoVerifyapplies to the whole intent filter. Mixingmyott://with HTTPS hosts makes verification fail for every host in it. Keep them in separate filters. - Every host listed separately.
example.comandwww.example.comare verified independently, and each must serve its ownassetlinks.json.
Since Android 12, unverified HTTPS links go straight to the browser instead of showing an app chooser, so broken verification that used to look like "one extra tap" now looks like "the link does not work". Run adb shell pm get-app-links com.example.ott on an Android 12 or later device to see the verification state for each host. Deeplinkly's glossary entry on Android App Link verification explains each state it reports.

Route the link to the right screen in the app
Verification only gets the URL into your app. What happens next decides whether the viewer watches.
Handle the incoming link in one place on each platform, parse the path into a route such as Show(48213) or Watch(91022), and run the same checks your normal navigation runs:
- Profile selection. If the household has profiles, ask once, then continue to the destination. Kids profiles should not open a link to a mature title.
- Sign-in and entitlement. Store the destination, send unsigned users through login or sign-up, then resume. If the title needs a plan the user lacks, show a paywall that names the show.
- Availability. Titles expire and rights vary by country. Open the series page or a related title rather than an error.
- Playback start. For
/watchlinks, open the player screen ready to play, not auto-playing with sound, unless the user tapped an explicit play action. - Analytics. Record which link opened the app so share and campaign links show up in reporting. Our video analytics software guide covers the event model.
Do not forget the warm-start case. A link tapped while the app is already running arrives through a different callback from a cold start on both platforms, and it is the path most often left untested.
Living-room devices are a separate job. Roku, Fire TV, Apple TV, Android TV, Tizen, and webOS each have their own launch-parameter or deep-link mechanism, covered in our smart TV app development guide.
Debug links that open the browser instead of the app
When a link opens the website, work from the domain inward.
| Symptom | Likely cause | Fix |
|---|---|---|
| Opens browser on every iOS device | AASA unreachable, redirected, or invalid JSON | Serve a direct 200 over HTTPS at /.well-known/ |
| iOS opens the app for some paths only | A rule order problem or missing component | Put exclusions first; test paths against the file |
| iOS fix not taking effect | Apple's CDN still has the old file | Wait for the cache to refresh; reinstall to re-fetch |
| Android opens Chrome after a release | Fingerprint does not match the Play-signed build | Use the Play Console fingerprint |
Android legacy_failure state | Domain returned the wrong file, type, or a redirect | Serve application/json with a direct 200 |
| Works when tapped, not when typed | Expected: typing a URL into the browser never opens the app | Test by tapping links in Notes, Messages, or email |
| Opens the website inside a social app | Some in-app browsers do not hand links to the OS | Show an explicit "Open in app" button on the web page |
Deeplinkly's Deep Link Debugger checks your live domain files, which covers the most common half of these problems in one step.
Build it yourself or use a link service
You can host the AASA and assetlinks files on your own domain and handle routing in the app. That covers installed users completely. What it does not give you is deferred deep linking for new installs, click and install attribution, or short branded campaign links with analytics.
Deeplinkly adds those on top of the same standards. It supports branded custom link domains, provides SDKs for iOS, Android, Flutter, React Native, and web, and its iOS SDK docs and Android SDK docs cover the app-side setup step by step. When Apexnova builds a streaming app, link routing is part of the navigation architecture from the first sprint rather than something bolted on before launch.
Frequently asked questions
Why does my Universal Link open Safari instead of my streaming app?
The most common causes are an AASA file that redirects, returns an error, or is not valid JSON; a missing applinks: entry in Associated Domains; or an exclusion rule that matches before the path you expect. Typing a URL directly into Safari also never opens the app.
Do Universal Links work if the app is not installed?
The link still works, but it opens your website instead of the app. To send new users through the App Store and then into the right show on first launch, you need deferred deep linking on top of Universal Links.
How long does an AASA file change take to reach users?
iOS gets the file through Apple's CDN and refreshes it on install or app update, so a change is not instant. Deleting and reinstalling the app on a test device forces a fresh fetch.
Which SHA-256 fingerprint goes in assetlinks.json when using Play App Signing?
Use the app signing key certificate fingerprint shown in the Play Console, because Google signs the version users install. Add your upload or debug fingerprint too if you need links to work on those builds.
Should a streaming app use custom URL schemes at all?
Keep a custom scheme only for cases that need it, such as returning from a payment or sign-in flow. For anything shared or promoted, use HTTPS Universal Links and App Links, which have a web fallback and cannot be claimed by another app.
Ship links that open the show every time
If your shares, emails, and push notifications are opening the browser, the fix is usually a day of careful work: clean URL design, correct domain files, separate intent filters, and routing that respects profiles, sign-in, and rights. If you also run paid campaigns to new users, plan for deferred deep linking and attribution at the same time so you only set up your link domain once.
Generate your AASA and assetlinks.json files free with Deeplinkly, or talk to Apexnova if you want deep linking built into your streaming app's navigation from day one.