A referral link should work the same whether the recipient is on iOS, Android, or web. Tap the link, see the referral content, and attribute the referral to the sender. In practice, each platform handles URLs differently, and the recipient might not have the app installed. This article covers how to build referral links that handle all these cases.
For referral deep link mechanics, see how referral deep links work: end-to-end guide. For building effective referral programs, see building referral programs that actually work.
The Problem
A referral link like https://yourdomain.com/referral/user123 needs to:
- Open the native app if installed (iOS or Android).
- Navigate to the correct screen with the referrer's ID.
- If the app is not installed, take the user to the app store or a landing page.
- After installation, still attribute the referral to
user123.
Each requirement has platform-specific challenges:
| Requirement | iOS | Android | Web |
|---|---|---|---|
| Open app | Universal Links | App Links | N/A |
| Navigate to screen | AppDelegate/SceneDelegate | Intent | URL routing |
| App not installed | Fallback to App Store | Fallback to Play Store | Show landing page |
| Post-install attribution | Deferred deep link | Deferred deep link | Cookie/localStorage |
Referral Link Structure
Use a consistent URL format that works across platforms:
https://yourdomain.com/referral/{referrerId}?campaign={campaignId}
The URL is the same for all platforms. Platform detection happens server-side or client-side when the link is opened.
Server-Side Detection
When the URL is opened in a browser (app not installed), detect the platform and redirect:
// Server route handler
app.get('/referral/:referrerId', (req, res) => {
const { referrerId } = req.params;
const userAgent = req.headers['user-agent'] || '';
// If the app handles this via Universal Links / App Links,
// this handler won't be called. This runs only when the app
// isn't installed or links aren't verified.
if (/iPhone|iPad|iPod/.test(userAgent)) {
// iOS: redirect to App Store with context
res.redirect(
`https://apps.apple.com/app/id${APP_STORE_ID}?referrer=${referrerId}`
);
} else if (/Android/.test(userAgent)) {
// Android: redirect to Play Store with referrer
res.redirect(
`https://play.google.com/store/apps/details?id=${PACKAGE_NAME}&referrer=${referrerId}`
);
} else {
// Web: show referral landing page
res.render('referral-landing', { referrerId });
}
});
Native App Handling
iOS
Configure Universal Links so https://yourdomain.com/referral/* opens the app:
AASA file:
{
"applinks": {
"details": [{
"appIDs": ["TEAMID.com.yourcompany.yourapp"],
"components": [
{ "/": "/referral/*" }
]
}]
}
}
Handle in the app:
func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
guard let url = userActivity.webPageUrl,
let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
return false
}
let path = components.path
if path.hasPrefix("/referral/") {
let referrerId = String(path.dropFirst("/referral/".count))
handleReferral(referrerId: referrerId)
return true
}
return false
}
func handleReferral(referrerId: String) {
// Store the referral
UserDefaults.standard.set(referrerId, forKey: "pending_referrer")
// Navigate to referral welcome screen
let vc = ReferralWelcomeViewController(referrerId: referrerId)
rootNavigationController?.pushViewController(vc, animated: true)
}
Android
Intent filter:
<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="/referral" />
</intent-filter>
Handle in the activity:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleReferralIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleReferralIntent(intent)
}
private fun handleReferralIntent(intent: Intent) {
val uri = intent.data ?: return
if (uri.path?.startsWith("/referral/") == true) {
val referrerId = uri.lastPathSegment ?: return
handleReferral(referrerId)
}
}
private fun handleReferral(referrerId: String) {
// Store the referral
getSharedPreferences("referral", MODE_PRIVATE)
.edit()
.putString("pending_referrer", referrerId)
.apply()
// Navigate to referral screen
val intent = Intent(this, ReferralWelcomeActivity::class.java).apply {
putExtra("referrer_id", referrerId)
}
startActivity(intent)
}
Deferred Deep Linking
The hardest case: the user taps the referral link but does not have the app installed. After they install and open the app, the referral should still be attributed.
How Deferred Deep Links Work
- User taps
https://yourdomain.com/referral/user123. - App not installed; user lands on a web page or app store.
- The referral context (referrer ID, timestamp, campaign) is stored server-side, associated with a fingerprint (IP, device info, timestamp).
- User installs the app and opens it.
- On first launch, the app calls a server endpoint with its fingerprint.
- Server matches the fingerprint and returns the original referral context.
- App processes the referral.
Server-Side Fingerprint Storage
// When the referral link is opened in a browser
app.get('/referral/:referrerId', async (req, res) => {
const { referrerId } = req.params;
// Store fingerprint for deferred matching
const fingerprint = {
ip: req.ip,
userAgent: req.headers['user-agent'],
timestamp: Date.now(),
referrerId,
campaign: req.query.campaign
};
await storeDeferredLink(fingerprint);
// Redirect to app store or landing page
redirectByPlatform(req, res, referrerId);
});
// When the app calls on first launch
app.post('/api/deferred-link', async (req, res) => {
const { ip, userAgent, deviceId } = req.body;
const match = await findDeferredLink({
ip,
userAgent,
maxAge: 48 * 60 * 60 * 1000 // 48 hours
});
if (match) {
res.json({
found: true,
referrerId: match.referrerId,
campaign: match.campaign
});
// Mark as consumed
await markDeferredLinkUsed(match.id);
} else {
res.json({ found: false });
}
});
Client-Side (First Launch)
// iOS: check for deferred link on first launch
func checkDeferredLink() {
guard !UserDefaults.standard.bool(forKey: "deferred_link_checked") else {
return
}
UserDefaults.standard.set(true, forKey: "deferred_link_checked")
let body: [String: Any] = [
"deviceId": UIDevice.current.identifierForVendor?.uuidString ?? "",
"userAgent": "iOS/\(UIDevice.current.systemVersion)"
]
// POST to your server
apiClient.post("/api/deferred-link", body: body) { result in
if let referrerId = result.referrerId {
self.handleReferral(referrerId: referrerId)
}
}
}
Attribution Consistency
Referral attribution must be consistent regardless of how the user arrived. Track the referral source for analytics:
interface ReferralAttribution {
referrerId: string;
platform: 'ios' | 'android' | 'web';
method: 'direct' | 'deferred'; // Direct = app was installed; Deferred = installed after
campaign?: string;
timestamp: number;
}
function attributeReferral(attribution: ReferralAttribution) {
// Store locally
localStorage.setItem('referral', JSON.stringify(attribution));
// Send to server
api.post('/api/referral/attribute', attribution);
}
Preventing Double Attribution
A referral should only be attributed once. Guard against duplicate attributions:
async function processReferral(referrerId: string, method: string) {
// Check if already attributed
const existing = await getReferralAttribution(userId);
if (existing) {
console.log('Referral already attributed:', existing.referrerId);
return;
}
// Store attribution
await storeReferralAttribution({
userId,
referrerId,
method,
timestamp: Date.now()
});
// Credit the referrer
await creditReferrer(referrerId);
}
Landing Pages for Web Users
When a desktop user clicks a referral link, they cannot install a mobile app. Show a landing page that:
- Explains the referral offer.
- Provides app store links (with referral context).
- Allows web signup (if applicable).
<!-- referral-landing.html -->
<div class="referral-page">
<h1>You've been invited!</h1>
<p>Your friend shared something with you.</p>
<div class="app-links">
<a href="https://apps.apple.com/app/idYOUR_ID"
class="app-store-badge">
Download on the App Store
</a>
<a href="https://play.google.com/store/apps/details?id=com.yourapp"
class="play-store-badge">
Get it on Google Play
</a>
</div>
<p>Or <a href="/signup?ref=USER123">sign up on the web</a></p>
</div>
Testing Referral Links
Direct Attribution (App Installed)
# iOS
xcrun simctl openurl booted "https://yourdomain.com/referral/user123?campaign=summer"
# Android
adb shell am start -a android.intent.action.VIEW \
-d "https://yourdomain.com/referral/user123?campaign=summer" \
-c android.intent.category.BROWSABLE
Verify:
- The app opens to the referral screen.
- The referrer ID
user123is stored. - Analytics show
method: direct.
Deferred Attribution (App Not Installed)
- Open the referral link in a mobile browser.
- Verify the user lands on the app store or landing page.
- Install the app.
- Open the app and verify the referral is attributed.
Web Attribution
- Open the referral link on desktop.
- Verify the landing page shows.
- Click "sign up on web."
- Verify the referrer ID is passed to the signup form.
Tolinku for Referral Links
Tolinku provides built-in referral program support with referral links that work across platforms. Tolinku handles deferred deep linking, so referral attribution works even when users install the app after clicking. The platform also provides a referral leaderboard and rewards tracking.
For referral mechanics, see how referral deep links work: end-to-end guide. For the cross-platform deep linking guide, see cross-platform deep linking guide for 2026.
Get deep linking tips in your inbox
One email per week. No spam.