LinkTrace

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.

01

Create the Link — Encode the Target Screen

Pass screenName (and any extra context) inside customPayload

API Request

http
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

json
{
  "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

screenNameNavigates toTypical use case
"promo_screen"A promotional / offer screenReferral reward, discount code landing
"onboarding_variant_b"Alternate onboarding flowA/B testing a different welcome sequence
"product_detail"Specific product / item pageSocial share of a product listing
"challenge_invite"In-app challenge or eventFriend invited to join a specific game/event
"profile"Another user's profileFollow / connect flows from social share
"referral_success"Referral reward confirmation screenClose 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

02

User Taps the Link

LinkTrace records the click, fingerprints the device, and redirects to the store

1

LinkTrace logs the click and records the device fingerprint

2

The customPayload is stored server-side, tied to the fingerprint

3

User is redirected to the App Store (iOS) or Play Store (Android)

Attribution window: user must install within 24 hours of clicking

user installs & creates an account

03

Call the Attribution API — After Sign-Up

Call this once, right after the user completes registration

API Request

http
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

json
{
  "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

04

Navigate to the Target Screen

Read customPayload.screenName from the response and route the user

1

Check attributed: true in the response

2

Read customPayload.screenName to determine destination

3

Pass any additional payload fields (e.g. promoCode) as parameters to the screen

4

If attributed: false — continue with the default post-signup flow

What you can do with the payload on this screen

Auto-apply a promo code from customPayloadPre-credit the referrer (referrerIdentifier)Show a personalised onboarding variantTag the user's session with campaign & sourceTrigger a referral reward flow

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.

swift
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.

swift
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.

swift
// 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.

swift
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).