Deferred Deep Linking
Route newly installed users to any screen inside your app — even before the app exists on their device.
What is Deferred Deep Linking?
A standard deep link (e.g. myapp://promo/SAVE20) works only if the app is already installed. Tap the same link on a device without the app and it does nothing useful.
Deferred deep linking bridges this gap. A user taps a link, gets sent to the App Store or Play Store, installs your app, opens it for the first time — and still lands on the exact screen the link was pointing to. The "deferred" part means the navigation intent survives the install gap.
LinkTrace implements this through device fingerprint matching combined with the customPayload field. You encode a screenName (and any extra context) into the link at creation time. On first launch after sign-up, your app calls the Attribution API — if the install is matched, the original payload is returned and you navigate accordingly.
Deeplink Flow
Trace a single tap from link to landing screen — created on your server, surviving the App Store install, and resolved on the user's first launch.
Create the Link — Encode the Target Screen
Pass screenName (and any extra context) inside customPayload
API Request
POST https://api.linktrace.in/api/v1/referral-links
Content-Type: application/json
x-api-key: YOUR_API_KEY
{
"referrerIdentifier": "user_9821",
"campaign": "summer_referral",
"customPayload": {
"screenName": "promo_screen",
"promoCode": "SAVE20",
"variant": "hero_cta"
}
}Response — 201 Created
{
"success": true,
"data": {
"shortCode": "56ff9241",
"referralLink": "https://app.linktrace.in/r/56ff9241",
"referrerIdentifier": "user_9821"
}
}Tip — customPayload limit
Up to 3 key-value string pairs. Use them for screen name, promo code, or onboarding variant — they are returned verbatim on a successful attribution.
Common screenName Values
| screenName | Navigates to | Typical use case |
|---|---|---|
| "promo_screen" | A promotional / offer screen | Referral reward, discount code landing |
| "onboarding_variant_b" | Alternate onboarding flow | A/B testing a different welcome sequence |
| "product_detail" | Specific product / item page | Social share of a product listing |
| "challenge_invite" | In-app challenge or event | Friend invited to join a specific game/event |
| "profile" | Another user's profile | Follow / connect flows from social share |
| "referral_success" | Referral reward confirmation screen | Close the loop — show what the referrer sent |
All customPayload constraints and the full referralLink response schema are in the API Reference →
share the short link
User Taps the Link
LinkTrace records the click, fingerprints the device, and redirects to the store
LinkTrace logs the click and records the device fingerprint
The customPayload is stored server-side, tied to the fingerprint
User is redirected to the App Store (iOS) or Play Store (Android)
user installs & creates an account
Call the Attribution API — After Sign-Up
Call this once, right after the user completes registration
API Request
POST https://api.linktrace.in/api/v1/attributions
Content-Type: application/json
x-api-key: YOUR_API_KEY
{
"userId": "your_internal_user_id"
}Success Response — 200 OK
{
"success": true,
"data": {
"attributed": true,
"referrerIdentifier": "user_9821",
"campaign": "summer_referral",
"customPayload": {
"screenName": "promo_screen",
"promoCode": "SAVE20",
"variant": "hero_cta"
}
}
}Important — call once per user
Trigger this call once right after sign-up completes — not on every app open. Repeated calls for the same userId may produce unexpected results. Gate it behind a flag (e.g. hasCheckedAttribution) persisted in local storage.
Full response fields and error codes in the API Reference →
read screenName from response and navigate
Navigate to the Target Screen
Read customPayload.screenName from the response and route the user
Check attributed: true in the response
Read customPayload.screenName to determine destination
Pass any additional payload fields (e.g. promoCode) as parameters to the screen
If attributed: false — continue with the default post-signup flow
What you can do with the payload on this screen
Platform Examples
Drop-in code for reading the deferred payload and routing to the right screen on first launch, for each major mobile stack.
1 — Attribution Model
Define a Swift struct that maps the attribution response. This lives in your network/models layer.
struct AttributionResponse: Codable {
let success: Bool
let data: AttributionData
}
struct AttributionData: Codable {
let attributed: Bool
let referrerIdentifier: String?
let campaign: String?
let source: String?
let customPayload: [String: String]?
}2 — Attribution Service
A lightweight async function that calls the Attribution API. Call this from your ViewModel or AppDelegate after sign-up.
final class AttributionService {
private let apiKey = "YOUR_API_KEY"
private let baseURL = "https://api.linktrace.in"
func fetchAttribution(userId: String) async throws -> AttributionData {
guard let url = URL(string: "\(baseURL)/api/v1/attributions") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(apiKey, forHTTPHeaderField: "x-api-key")
request.httpBody = try JSONEncoder().encode(["userId": userId])
let (data, _) = try await URLSession.shared.data(for: request)
let response = try JSONDecoder().decode(AttributionResponse.self, from: data)
return response.data
}
}3 — Navigation via NavigationStack
Use a typed route enum with NavigationStack and a navigationPath. Call checkDeferredDeepLink from the .onAppear of your post-signup root view, or directly from your sign-up completion handler.
// 1. Define your app's navigable routes
enum AppRoute: Hashable {
case promoScreen(promoCode: String?)
case onboardingVariantB
case productDetail(id: String)
case challengeInvite
case referralSuccess(referrerName: String?)
// add more routes as needed
}
// 2. Root view owning the NavigationStack
struct RootView: View {
@StateObject private var router = AppRouter()
var body: some View {
NavigationStack(path: $router.path) {
HomeView()
.navigationDestination(for: AppRoute.self) { route in
switch route {
case .promoScreen(let code):
PromoView(promoCode: code)
case .onboardingVariantB:
OnboardingVariantBView()
case .productDetail(let id):
ProductDetailView(productId: id)
case .challengeInvite:
ChallengeInviteView()
case .referralSuccess(let name):
ReferralSuccessView(referrerName: name)
}
}
}
.environmentObject(router)
}
}
// 3. Router class — holds path and resolution logic
@MainActor
final class AppRouter: ObservableObject {
@Published var path = NavigationPath()
private let service = AttributionService()
private let defaults = UserDefaults.standard
func checkDeferredDeepLink(userId: String) {
// Guard: only run once per user
guard !defaults.bool(forKey: "lt_attribution_checked") else { return }
Task {
do {
let data = try await service.fetchAttribution(userId: userId)
defaults.set(true, forKey: "lt_attribution_checked")
guard data.attributed,
let payload = data.customPayload,
let screenName = payload["screenName"] else { return }
navigate(to: screenName, payload: payload)
} catch {
// Attribution failure is non-fatal; continue default flow
}
}
}
private func navigate(to screenName: String, payload: [String: String]) {
switch screenName {
case "promo_screen":
path.append(AppRoute.promoScreen(promoCode: payload["promoCode"]))
case "onboarding_variant_b":
path.append(AppRoute.onboardingVariantB)
case "product_detail":
guard let id = payload["productId"] else { return }
path.append(AppRoute.productDetail(id: id))
case "challenge_invite":
path.append(AppRoute.challengeInvite)
case "referral_success":
path.append(AppRoute.referralSuccess(referrerName: nil))
default:
break
}
}
}4 — Trigger After Sign-Up
Inject the router into your sign-up view and call checkDeferredDeepLink as soon as the user's account is created.
struct SignUpView: View {
@EnvironmentObject private var router: AppRouter
@State private var isLoading = false
var body: some View {
VStack {
// ... sign-up form UI ...
Button("Create Account") {
Task { await handleSignUp() }
}
}
}
private func handleSignUp() async {
isLoading = true
defer { isLoading = false }
// 1. Create the user account in your backend
let userId = try await AuthService.signUp(...)
// 2. Check for a deferred deep link — navigates if attributed
router.checkDeferredDeepLink(userId: userId)
}
}Best Practices
A short checklist to keep deferred routing reliable — gate it to first launch, always handle the no-match case, and fall back gracefully.
Gate behind a one-time flag
Persist a hasCheckedAttribution boolean in local storage. Check it before calling the Attribution API — this prevents duplicate calls on subsequent launches.
Always handle attributed: false
Attribution failure or no-match is the common case for organic installs. Your app must have a clean default post-signup flow that runs when no deep link is present.
Keep screenName values in a shared constant
Define screen name strings in a single file (e.g. DeepLinkScreens.kt / DeepLinkRoutes.swift) shared between link creation and navigation logic to avoid typo mismatches.
Test with environment: "stage"
Always create test links with "environment": "stage" to validate the full deferred deep linking flow end-to-end without polluting production attribution data.
Attribution window is 24 hours
The user must install within 24 hours of clicking the link. If the window expires, the Attribution API returns attributed: false — your default flow takes over. Set expectations accordingly in QA.
customPayload values must be strings
All values in customPayload are strings. For numeric IDs or booleans, stringify on encode and parse on receipt (e.g. "productId": "42", not "productId": 42).