Deeplinks
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.
Two link types#
Mobana links come in two flavours, each a URL on your {appId}.mobana.ai host. Choose the path based on your intent:
- /link — Acquisition. Captures attribution and redirects straight to the App Store or Play Store. No app-open attempt, no timeout delay. Best for ad campaigns and new-user acquisition where a clean, uninterrupted store funnel matters.
- /deep — Deeplink. Registered as a Universal Link / App Link so the OS opens the installed app instantly. When the app is not installed, Mobana captures attribution, fires a scheme:// / intent:// app-open attempt, and falls back to the store after 2–3 s. Best for re-engagement — unlock codes, referral links, push notifications, share flows.
Both paths support the same payload parameters (data, UTM fields, custom params) and write an attribution Click. Your onDeepLink handler fires for both.
How direct opening works#
When a /deep link is tapped and your app is already installed, the OS intercepts it via Universal Links / App Links (see Universal Links / App Links below) and opens the app directly — no browser involved. If UL is not set up, the link falls through to the browser and Mobana fires your configured URL scheme to open the app:
- iOS — fires
myapp://abc123.mobana.ai/deep?…. If the app is installed, iOS switches to it immediately. The SDK receives the URL viaLinking, parses the payload, and firesonDeepLink. - Android — fires an
intent://URL with your package name. Chrome opens your app directly. The SDK receives the URL and firesonDeepLink.
If the app is not installed, the user goes through the App Store / Play Store. On first launch the SDK calls /find to match the install to the recorded Click and fires onDeepLink with source: 'deferred' — same payload.
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: Mobana fires
myapp://(iOS) orintent://(Android) to open your app directly. YouronDeepLinkhandler fires 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. - In-app browsers (Instagram, TikTok, etc.): scheme-based opening usually still works. If the app isn't installed, the browser redirects to the store, and the SDK probes on next launch with
source: 'probabilistic'.
Setup#
1. Configure your app in the Dashboard
In Dashboard → App Settings → Deeplinks, add:
- iOS URL Scheme — the custom URL scheme registered in your app's
Info.plist(e.g.myapp). Mobana firesmyapp://from the redirect page to open your app when it's installed. - iOS Team ID (optional) — enables Universal Links so iOS can intercept the URL before opening the browser at all. See Universal Links / App Links.
- Android SHA-256 fingerprints (optional) — enables Android App Links for the same OS-level intercept on Android.
Android intent:// opening uses your app's Package Name, which you already configured in App Details when adding your Android platform. No additional dashboard field needed.
2. Register the iOS URL Scheme in your app
Bare React Native — add to ios/YourApp/Info.plist:
<!-- ios/YourApp/Info.plist -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>Expo — add scheme to app.json:
{
"expo": {
"scheme": "myapp"
}
}The value in Info.plist / app.json must exactly match the iOS URL Scheme you entered in the Dashboard (case insensitive). If they differ, iOS won't open your app when the scheme URL fires.
3. Add an Android intent-filter
Add an <intent-filter> to your launcher activity in AndroidManifest.xml. This lets the Android Linking API receive the URL from Mobana's intent:// redirect:
<!-- AndroidManifest.xml — inside the launcher <activity> -->
<!-- Required for intent:// deep link routing. The pathPrefix scopes this
filter to Mobana links only — without it, any https:// link on your
Mobana host could open the app. autoVerify enables App Links once you
add SHA-256 fingerprints in App Settings → Deeplinks -->
<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="/deep" />
<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/deep" />
</intent-filter>Without android:pathPrefix="/deep", Android may route taps on /find, /conversion, and other Mobana paths through your app instead of the browser. The pathPrefix scopes this filter to /deep links only.
4. 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: scheme open, 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 — scheme open, 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>Expo Router#
This step is only needed if your app uses Expo Router (file-based routing). Bare React Native and React Navigation work out of the box — skip ahead.
Expo Router evaluates every incoming URL against your routes and renders an "Unmatched Route" screen for anything it can't match. Mobana links (https://abc123.mobana.ai/link?… and the myscheme:// opens) aren't app routes — the SDK consumes them via Linking — so Expo Router shows that error screen before you ever see the deeplink.
Expo Router provides a pre-routing hook, app/+native-intent.tsx, that filters URLs before they hit the router. The SDK ships a ready-made filter — drop it in and Mobana URLs are skipped while everything else routes normally:
// app/+native-intent.tsx
// Default endpoint ({appId}.mobana.ai), or a custom endpoint set via
// EXPO_PUBLIC_MOBANA_ENDPOINT — just re-export, no setup needed.
export { redirectSystemPath } from '@mobana/react-native-sdk/expo-router';Using a custom endpoint? Either set EXPO_PUBLIC_MOBANA_ENDPOINT (the re-export above picks it up automatically), or build the filter explicitly with the same endpoint you give Mobana.init():
// app/+native-intent.tsx
import { createRedirectSystemPath } from '@mobana/react-native-sdk/expo-router';
// Custom endpoint — pass the same endpoint you give Mobana.init().
export const redirectSystemPath = createRedirectSystemPath({
endpoint: 'https://go.yourapp.com/da',
});If you handle other links there too, compose the filter — run your own rules first, then hand the rest to Mobana:
// app/+native-intent.tsx
import { createRedirectSystemPath } from '@mobana/react-native-sdk/expo-router';
const filterMobana = createRedirectSystemPath();
export function redirectSystemPath({
path,
initial,
}: {
path: string;
initial: boolean;
}) {
// your own pre-routing rules here, then hand the rest to Mobana
return filterMobana({ path, initial });
}The SDK receives deeplinks via Linking regardless of this file, so attribution and onDeepLink keep firing. The filter's job is purely to suppress Expo Router's "Unmatched Route" screen for a clean user experience.
Universal Links / App Links (optional)#
Universal Links (iOS) and App Links (Android) let the OS intercept the Mobana URL before it reaches the browser — your app opens instantly with no browser tab, no spinner, no redirect delay.
These are optional. Scheme-based opening (the default) already works reliably for the vast majority of cases including in-app browsers. Universal Links / App Links add a smoother UX for direct taps in native apps (iMessage, Mail, etc.) when everything lines up.
Apple's CDN caches the AASA file for up to 7 days, including an empty/missing version. If iOS fetches it before you've entered your Team ID, Universal Links degrade to the browser path for up to a week. Add your credentials in the dashboard before adding the applinks: entitlement to your build.
iOS — Universal Links
1. Add your iOS Team ID to Dashboard → App Settings → Deeplinks. Mobana serves the apple-app-site-association file automatically. 2. In Xcode → Signing & Capabilities → Associated Domains, add:
applinks:YOUR_APP_ID.mobana.ai
# Or, when using a custom endpoint:
applinks:yourdomain.comFinding your iOS Team ID
- Sign in to App Store Connect → Users and Access → Integrations, or the Apple Developer portal → Account → Membership Details.
- Find the Team ID field — a 10-character alphanumeric string, e.g.
ABCDE12345. - Paste it into Dashboard → App Settings → Deeplinks → iOS Team ID.
On debug/dev-signed builds, append ?mode=developer to the applinks: entry. iOS fetches AASA directly from your server on every install instead of Apple's CDN — eliminating the 7-day cache wait when iterating on setup.
Android — App Links
1. Add your SHA-256 signing certificate fingerprint(s) to Dashboard → App Settings → Deeplinks. 2. Add android:autoVerify="true" to your existing intent-filter:
<!-- Add android:autoVerify="true" and configure SHA-256 fingerprints
in App Settings → Deeplinks to enable App Links (OS-level intercept). -->
<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="/deep" />
</intent-filter>Finding your SHA-256 fingerprints
Option A — keytool
keytool -list -v -keystore /path/to/your.keystore -alias your_key_aliasOption B — Play App Signing
If you use Google Play App Signing (default since 2021): Play Console → your app → Release → Setup → App integrity → copy the SHA-256 under App signing key certificate.
Add both debug and release fingerprints (and Play App Signing if applicable). Missing one means App Links won't verify for that build variant — Android silently falls back to the browser for those builds.
Using a custom domain (e.g. yourdomain.com/da)? Your reverse proxy must forward /.well-known/apple-app-site-association and /.well-known/assetlinks.json at the host root. The nginx / Apache templates in App Settings → Custom Endpoint already include these. See the Custom Endpoints guide.
Delivery matrix#
What fires for each combination of install state, attribution state, and direct-open success. 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 | App opens via | getAttribution() returns | onDeepLink fires |
|---|---|---|---|---|
| Yes | Yes | scheme / UL | (cached, unchanged) | universal_link |
| Yes | Yes | scheme fails → store | (cached, unchanged) | probabilistic (probe) |
| Yes | No | scheme / UL | matched on first launch | universal_link |
| Yes | No | scheme fails → store | matched on first launch | deferred |
| No | — | App Store / Play Store | 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 (scheme open, 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 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 scheme URL 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. - Custom URL schemes can be registered by any app. If your scheme name is generic (e.g.
app), another installed app could intercept it. Use a unique scheme likecom.yourcompany.yourappor your brand name. Universal Links (when configured) are cryptographically verified and immune to hijacking. - 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. - 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).