> For the complete documentation index, see [llms.txt](https://docs.amply.tools/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.amply.tools/developer-guide/handling-deeplinks.md).

# Handling deeplinks

When a campaign fires, Amply tells your app where to go — a screen, a sheet, a URL — by sending a **deeplink**. Your app decides what that URL means and whether it was handled.

A PM should read this as: "when a campaign targets this user, the SDK hands the app a URL with some metadata, and the app navigates accordingly. The developer wires that URL into their app's navigation."

**Use this when** you run campaigns that route users to screens, external URLs, or in-app surfaces. **Don't use this when** a campaign only needs to record an event or set a property — no deeplink is involved in those cases.

## What the SDK delivers

When a campaign matches the current user and fires, the SDK invokes your registered listener with:

* **`url`** — the target URL chosen by the campaign (`yourapp://promo/annual-upgrade`, `https://example.com/help`, or a custom scheme you own)
* **`info`** — a map of metadata the campaign attached to this deeplink (campaign ID, variant, anything the PM configured)
* **`consumed`** (React Native only) — whether another listener in the chain already handled this URL

Your listener returns `true` if it handled the URL, `false` otherwise. Returning `true` lets the SDK know the deeplink was acted on.

## Registering a listener

{% tabs %}
{% tab title="iOS (Swift)" %}

```swift
import AmplySDK

class AppDeepLinkHandler: DeepLinkListener {
    func onDeepLink(url: String, info: [String: Any]) -> Bool {
        print("Amply deeplink: \(url) info: \(info)")

        if url.hasPrefix("yourapp://promo/") {
            // Navigate to promo screen
            Router.shared.showPromo(url: url)
            return true
        }

        // Let another handler deal with it
        return false
    }
}

// Somewhere during app startup, after SDK init:
let handler = AppDeepLinkHandler()
let deepLinkToken = amply.registerDeepLinkListener(listener: handler)
```

Hold a strong reference to your listener from your own side so it stays alive for the lifetime of the app, and keep the returned token - `removeDeepLinkListener(token:)` takes the token, not the listener. A listener that lives for the whole app never needs detaching; one owned by a single screen does.
{% endtab %}

{% tab title="Android (Kotlin)" %}

```kotlin
import tools.amply.sdk.actions.DeepLinkListener

class AppDeepLinkHandler : DeepLinkListener {
    override fun onDeepLink(url: String, info: Map<String, Any>): Boolean {
        Log.d("Amply", "deeplink: $url info: $info")

        if (url.startsWith("yourapp://promo/")) {
            Router.showPromo(url)
            return true
        }

        return false
    }
}

// During Application.onCreate(), after amply init:
val handler = AppDeepLinkHandler()
val deepLinkToken = amply.registerDeepLinkListener(handler)
```

{% endtab %}

{% tab title="React Native (TS)" %}

```ts
import Amply from '@amplytools/react-native-amply-sdk';
import { useEffect } from 'react';

export function useAmplyDeepLinks(onPromo: (url: string) => void) {
  useEffect(() => {
    let unsubscribe: (() => void) | undefined;
    let unmounted = false;

    Amply.addDeepLinkListener(event => {
      console.log('Amply deeplink:', event.url, event.info, 'consumed=', event.consumed);

      if (event.url.startsWith('yourapp://promo/')) {
        onPromo(event.url);
      }
    })
      .then(unsub => {
        if (unmounted) unsub();
        else unsubscribe = unsub;
      });

    return () => {
      unmounted = true;
      unsubscribe?.();
    };
  }, [onPromo]);
}
```

`addDeepLinkListener` returns a `Promise<() => void>`. Await it to get the unsubscribe function, and call it on unmount.
{% endtab %}
{% endtabs %}

## Return value (iOS + Android)

`onDeepLink` returns `Boolean`:

* `true` — your listener handled the URL. The SDK treats it as consumed.
* `false` — your listener did not handle this URL. The SDK may hand it to another registered listener, or fall through.

On React Native, the listener does not return a value. Instead, the SDK hands you a `consumed` flag that tells you whether an earlier subscriber already handled this URL.

## Multiple listeners

All three SDKs support multiple listeners. Each listener sees every deeplink. The native iOS and Android SDKs consider the deeplink "consumed" if any listener returns `true`. On React Native, listeners receive a `consumed` flag indicating whether another listener already handled this URL.

In practice, most apps register one listener at app startup and route by URL prefix.

## Withdrawing a listener (native iOS / Android)

Requires apps built with SDK 0.9.0 or later. Registering **adds** a listener rather than replacing one and returns a token, so if you register from an object that is torn down and rebuilt while your app keeps running, call `removeDeepLinkListener(token)` when that object goes away. Otherwise the old listeners stay registered and every later deeplink is delivered to all of them.

Withdrawing takes the **token** the registration returned, so keep the token — a listener written inline is fine. An app that registers once at startup, as above, never needs this. On React Native, unsubscribe with the function you get back from `addDeepLinkListener` — see below.

A listener that throws is treated as "did not handle it", and the URL still reaches the listeners registered after it.

## When the app should wait for the outcome

A deeplink listener is fire-and-forget: the SDK hands you the URL and the campaign is done. Some actions instead need the user's flow to **wait** for them — a rewarded ad before an export, a survey before a cancel. Those are **gates**: you register them separately and the gated call site awaits the outcome. See [Gating actions](/developer-guide/gating-actions.md). Everything you don't register as a gate stays an ordinary deeplink, handled here.

## React Native: unsubscribing

`addDeepLinkListener` is async and returns an unsubscribe function. Always unsubscribe when the listener's owning component unmounts — otherwise closed-over state will leak:

```ts
useEffect(() => {
  let unsubscribe: (() => void) | undefined;
  Amply.addDeepLinkListener(handler).then(unsub => { unsubscribe = unsub; });
  return () => { unsubscribe?.(); };
}, []);
```

For a full teardown (e.g., sign-out), you can also call `Amply.removeAllListeners()` which unsubscribes every deep link listener the SDK tracks.

## URL schemes

`yourapp://` throughout these docs is a placeholder for **a scheme your app owns and registers** (in `Info.plist` on iOS, your intent filter on Android) — Amply doesn't reserve or ship a deeplink scheme. A campaign simply carries whatever URL string you configure: a custom scheme like `yourapp://promo/123`, or an external `https://` link. Treat the `url` field as an opaque string; parse it however your app normally parses URLs.

External `https://` URLs coming from campaigns are typically opened by native code through the system's URL handling. The deeplink listener still receives them so the app can log or suppress them.

## Testing

You can simulate a campaign-style deeplink from the terminal:

```bash
# iOS Simulator
xcrun simctl openurl booted "yourapp://promo/test"

# Android device or emulator
adb shell am start -a android.intent.action.VIEW -d "yourapp://promo/test" <your.package.name>
```

Note: these open the OS-level URL. Campaign-driven deeplinks are triggered by matching a real campaign; see [Testing your integration](/developer-guide/testing-your-integration.md) for how to verify end-to-end.

## Related

* [Gating actions](/developer-guide/gating-actions.md) — for actions the user's flow must wait on (rewarded ad, survey)
* [Showing custom popups](/developer-guide/showing-custom-popups.md) — when the campaign wants the app to render a popup
* [Handling callbacks](/developer-guide/handling-callbacks.md) — other SDK events (session, config, init)
* [Concepts — Scenarios and campaigns](/concepts/scenarios-and-campaigns.md) — how the dashboard decides what deeplink to send
* [iOS SDK reference](/reference/sdk-ios.md) — exact signatures
* [Android SDK reference](/reference/sdk-android.md) — exact signatures
* [React Native SDK reference](/reference/sdk-react-native.md) — exact signatures
