Deeplinks & Universal Links
One Mobana URL that opens your app directly when installed, falls through to install + deferred deeplink when not, and recovers from in-app browser failures via a re-engagement probe.
One link, every case#
Every Mobana link is a URL on your {appId}.mobana.ai/link path. The same URL covers three jobs that used to need three different products:
- Acquisition / attribution — UTM-tagged campaign links, MMP-style tracking templates, click-IDs from ad networks.
- Deferred deeplinking — a payload tapped pre-install shows up in your app on first launch (unlock codes, referral IDs, in-app routing).
- Re-engagement — a returning user tapping a push, email, share-sheet, or QR link gets their app opened directly via Universal Links / App Links, with the same payload fired through
onDeepLink.
You do not pick a path per link. Add a data payload when you want your app to act on the tap; leave it off for a vanilla campaign link. Your onDeepLink handler decides what to do with each event — typically branching on event.data?.something and ignoring the rest.
A concrete example#
You DM a creator their unique link:
https://YOUR_APP_ID.mobana.ai/link
?data={"unlock":"PROMO50","ref":"creator123"}
&utm_source=tiktok
&utm_campaign=influencer_dropWhen the creator's audience taps it:
- App installed: the OS opens your app directly (Universal Link / App Link). Your
onDeepLinkhandler fires within ~50 ms with the parsed payload. - App not installed: the user goes through the App Store / Play Store. On first launch the SDK matches the click and fires
onDeepLinkwithsource: 'deferred'— same payload. - App installed but UL didn't fire (in-app browsers like Instagram / TikTok strip Universal Links): the user lands in the App Store, taps Open, your app launches, the SDK probes the server, and your handler fires with
source: 'probabilistic'.
Setup#
1. Configure your app for Universal Links / App Links
In Dashboard → App Settings → Deeplinks, add your iOS Team ID and your Android SHA-256 fingerprint(s). Mobana serves the apple-app-site-association and assetlinks.json files automatically at https://{appId}.mobana.ai/.well-known/... — you don't host them yourself.
Finding your iOS Team ID
- Sign in to App Store Connect and navigate to Users and Access → Integrations (or open the Apple Developer portal and go to Account → Membership Details).
- Find the Team ID field. It is a 10-character alphanumeric string — e.g.
ABCDE12345. - Paste it into Dashboard → App Settings → Deeplinks → iOS Team ID.
Mobana combines the Team ID with your iOS Bundle ID (from App Details) to form the appIDs entry in the AASA file: TEAMID.com.yourcompany.yourapp. Make sure the Bundle ID in App Settings matches the one in your Xcode project exactly.
Finding your Android SHA-256 fingerprint(s)
You need one fingerprint per signing key (debug key, release key, and — if you use Play App Signing — the App Signing certificate). Add all of them so deeplinks work during development and in production.
Option A — keytool (local keystore)
Run this for each keystore file. The SHA-256 line under Certificate fingerprints is what you need.
keytool -list -v -keystore /path/to/your.keystore -alias your_key_aliasOption B — Android Studio
- Open your project in Android Studio.
- Open the Gradle panel (View → Tool Windows → Gradle) and run
signingReportunder Tasks → android. - Look for the SHA-256 value in the output for the relevant variant.
Option C — Play App Signing certificate
If you use Google Play App Signing (the default since 2021), Google re-signs your APK with their key. You must also add their certificate fingerprint:
- In the Google Play Console, go to your app → Release → Setup → App integrity.
- Copy the SHA-256 certificate fingerprint shown under App signing key certificate.
Missing a fingerprint means App Links won't verify for that build variant. Debug builds fail silently — the Android OS falls back to opening the browser instead of your app. Add both your debug and release fingerprints while developing.
Apple's CDN caches the AASA file for up to 7 days. If Apple fetches it before you've entered your Team ID, the empty version is locked in and Universal Links degrade to the web flow for up to a week. Add your credentials in the dashboard before submitting a build that includes the applinks: entitlement or the Android intent-filter.
2. Register the domain in your mobile app
iOS — Xcode → Signing & Capabilities → Associated Domains:
applinks:YOUR_APP_ID.mobana.ai
# Or, when using a custom endpoint:
applinks:yourdomain.comAndroid — AndroidManifest.xml:
<!-- AndroidManifest.xml — inside the launcher <activity> -->
<!-- Mobana subdomain (always required) -->
<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="YOUR_APP_ID.mobana.ai"
android:pathPrefix="/link" />
</intent-filter>
<!-- Custom domain — add this block only if you use a custom endpoint.
Each host needs its own <intent-filter>; mixing hosts in one filter
creates unintended host × path combinations. -->
<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="yourdomain.com"
android:pathPrefix="/da/link" />
</intent-filter>Without android:pathPrefix="/link", Android verifies your app for the entire host — meaning taps on /find, /conversion, and other Mobana internal paths could also try to open your app. The pathPrefix scopes App Link handling to /link URLs only, matching what Mobana's iOS AASA file already does on the Apple side.
If you use a custom endpoint, add a second <intent-filter> for your domain (see the snippet above) with the path prefix matching how your proxy forwards /link — typically /da/link. Do not combine two hosts in one filter: Android creates the cartesian product of all <data> elements, which produces unintended host + path combinations.
Using a custom domain (e.g. yourdomain.com/da)? Your reverse proxy must forward both /.well-known/apple-app-site-association and /.well-known/assetlinks.json to Mobana. The auto-generated proxy templates in App Settings → Custom Endpoint already include these. See the Custom Endpoints guide for details.
3. Wire your payload handler
Use onDeepLink() as the single surface for all routing actions — unlocks, referral codes, in-app navigation. It fires for every delivery case: Universal Link, deferred (first launch after install), and probe (re-engagement). You don't need to wire getAttribution() separately for routing — that's for install analytics only (campaign data, ad network click IDs).
import { Mobana } from '@mobana/react-native-sdk';
interface UnlockData { unlock?: string; ref?: string }
// onDeepLink fires for every Mobana link tap — Universal Link, deferred
// (first launch after install), and re-engagement probe. Branch on the
// payload: act on what you care about, ignore the rest as a vanilla
// campaign click.
Mobana.onDeepLink<UnlockData>((event) => {
if (event.data?.unlock) unlockPromo(event.data.unlock);
if (event.data?.ref) trackReferral(event.data.ref);
});If you already use <MobanaProvider> for flows, you can add onDeepLink directly to it:
<MobanaProvider
onDeepLink={(event) => {
if (event.data?.unlock) unlockPromo(event.data.unlock);
}}
>
<App />
</MobanaProvider>Delivery matrix#
What fires for each combination of install state / attribution state / Universal-Link interception. The takeaway: onDeepLink fires in every cell — wire your routing logic there and you don't need to special-case any delivery path.
| Installed | Attributed | UL fires | getAttribution() returns | onDeepLink fires |
|---|---|---|---|---|
| Yes | Yes | Yes | (cached, unchanged) | universal_link |
| Yes | Yes | No | (cached, unchanged) | probabilistic (probe) |
| Yes | No | Yes | matched on first launch | universal_link |
| Yes | No | No | matched on first launch | deferred |
| No | — | — | matched on first launch | deferred |
onDeepLink vs getAttribution()#
onDeepLink is the right place for all routing actions — unlocks, referral codes, screen navigation. It fires for every delivery case (Universal Link, deferred, probe), and the same payload arrives every time.
getAttribution() is for install attribution analytics: reading which campaign or channel brought the user, and reporting click IDs (ttclid, fbclid, gclid, etc.) back to ad networks. It returns whichever Click ended up matching this install regardless of whether the user tapped a plain campaign link or a payload-carrying one.
// getAttribution() is for install analytics — read the campaign + click IDs
// that brought the user and report them to your ad networks.
const result = await Mobana.getAttribution();
if (result.status === 'matched') {
analytics.track('install_attributed', {
utm_source: result.attribution?.utm_source,
utm_campaign: result.attribution?.utm_campaign,
});
if (result.attribution?.click_params?.ttclid) {
reportToTikTok({ ttclid: result.attribution.click_params.ttclid });
}
}Don't mirror onDeepLink routing logic in your getAttribution() path — that would double-execute on first launch (deferred deeplink also resolves via onDeepLink).
Privacy / tracking-disabled mode#
When you call Mobana.setTrackingEnabled(false) (or pass enableTracking: false to init()), Mobana links still deliver payloads — your in-app behaviour keeps working in all five cases above. What changes server-side:
- No
installIdis sent on probe / record calls. - Server doesn't write a per-device
DeeplinkClickrow. The probe still claims the matchedClickso it can't be re-fired, but no row links it to a device. /link/recordis not called at all when a Universal Link fires — the SDK parses the URL locally and surfaces it viaonDeepLinkonly.
Security notes#
- Don't put secrets in the URL. The
dataparam is visible to anyone who gets the link. Treat it like a signed coupon code, not a JWT. - Re-engagement window is intentionally short (5 min). A click can only trigger an
onDeepLinkon an installed device for 5 minutes after the tap. For deferred installs, the longer install-attribution window applies as usual. - Probe matches require a tight signal match. Confidence ≥ 0.9 (vs 0.7 for install attribution). If you see false negatives, check that your custom-endpoint proxy is forwarding the real client IP (
X-Real-IP).