# Deep linking

> Handle incoming deep links in your Android app with the Grovs SDK.

Source: https://docs.grovs.io/docs/sdk/android/deep-linking

Grovs delivers each opened link to a listener in your launcher activity. Route the user from there.

## Receiving deep link events

The listener gets the link, its custom payload and its tracking metadata:

```kotlin
Grovs.setOnDeeplinkReceivedListener(this) { deeplinkDetails ->
    val link = deeplinkDetails.link
    val payload = deeplinkDetails.data
    val tracking = deeplinkDetails.tracking

    payload?.get("screen")?.let { screen ->
        navigateTo(screen as String)
    }
}
```

It runs on the main thread. It fires for links that open the app, links delivered while it runs, deferred links matched after a fresh install, and (since 3.1) tapped Grovs push campaigns. See [What your app receives](https://docs.grovs.io/docs/sdk/android/push-notifications#what-your-app-receives) for the push case.

### Using Kotlin Flow

With coroutines, observe the `openedLinkDetails` flow. It emits the same `DeeplinkDetails`, or `null` until the app is opened from a link:

```kotlin
import io.grovs.utils.flow

lifecycleScope.launch {
    Grovs.Companion::openedLinkDetails.flow.collect { deeplinkDetails ->
        deeplinkDetails?.let {
            Log.d("Grovs", "Link: ${it.link}, data: ${it.data}")
        }
    }
}
```

### When the lookup fails

If the lookup fails (phone offline, backend unavailable), the SDK keeps the link and retries while the app stays open in the same session:

- after a short, widening wait
- when the app returns to the foreground
- as soon as the network comes back

The listener fires once the lookup succeeds. A link refused with a 4xx other than 429 isn't retried.

## Clipboard-assisted deferred deep linking

When a copy-to-clipboard link opens in a browser, the preview page copies it to the clipboard before sending the visitor to Google Play. On first launch after install, if fingerprint matching found nothing, the SDK checks the clipboard and attributes the install deterministically. The deferred link arrives through the same listener as a fingerprint match.

No integration is needed. What to expect:

- Android 12+ shows the system "pasted from your clipboard" toast once, on that first launch.
- The check is skipped when the backend reports no recent clicks on your clipboard-enabled links (currently a 48-hour window), and when the clipboard holds no web URL. An organic install on a project with active clipboard links may still see the toast once.
- Only URLs on accepted Grovs link hosts are sent. Hosts are checked after the text is read, so a non-Grovs URL can trigger the toast with nothing sent.

If your links use a custom domain, list it so the SDK recognizes them:

```kotlin
Grovs.configure(
    this,
    "your-api-key",
    useTestEnvironment = false,
    baseURL = null,
    autoTrackScreenViews = true,
    clipboardDomains = listOf("links.yourdomain.com")
)
```

Enable copy-to-clipboard per link with [`copyToClipboardAndroid`](https://docs.grovs.io/docs/sdk/android/link-creation), or set the project default in the dashboard. Flow, settings and limits: [Copy to Clipboard](https://docs.grovs.io/docs/links/copy-to-clipboard).

### Install attribution timing

Install attribution has one 25-second budget for the install-referrer lookup, fingerprint matching and the clipboard fallback. Lifecycle, purchase and custom events queued in that window are held so they credit the matched link. After the deadline they may upload unattributed; a later match still applies to events still queued.

A link that opened the app is attributed to new lifecycle and purchase events immediately and overrides a pending launch lookup. Custom events pick it up once the lookup completes.

## Getting link details

Fetch details for a link generated in the current environment, by path:

```kotlin
// Using a callback
Grovs.linkDetails(path = "/my-link-path", lifecycleOwner = this) { details, error ->
    details?.let { Log.d("Grovs", "Details: $it") }
    error?.let { Log.e("Grovs", "Error: $it") }
}

// Using coroutines
lifecycleScope.launch {
    try {
        val details = Grovs.linkDetails(path = "/my-link-path")
    } catch (e: GrovsException) {
        Log.e("Grovs", "Error: ${e.message}")
    }
}
```

The callback runs on the main thread and is cancelled if `lifecycleOwner` is destroyed first. The coroutine form throws `GrovsException` on failure.

## Next steps

- [Generate smart links →](https://docs.grovs.io/docs/sdk/android/link-creation)
- [Track events and screens →](https://docs.grovs.io/docs/sdk/android/analytics)
- [Full API reference →](https://docs.grovs.io/docs/sdk/android/api-reference)
