# Handle Genuin deep links in your app

Open shared Genuin video, community, group, and profile links inside your app instead of the browser, on Android, iOS, React Native, Flutter, and the web.

Every share link Genuin generates points at your hosted social experience's web address. When someone taps one on a phone that has your app, Android App Links or iOS universal links can open your app instead of the browser. Your app hands the link to the Genuin SDK, which looks up the item behind it and opens the matching screen.

This page covers Android and iOS SDK 2.1.5, genuin-react-native 3.0.3, genuin_sdk 1.0.1 for Flutter, and Web SDK 2.0.6. A shared link reaches your app in five steps:

1. A member taps **Share** on a video. The link uses your hosted social experience's domain, such as `https://community.example.com/video/{{VIDEO_SLUG}}`.
2. Someone opens the link on a phone. If your app is installed and verified for that domain, the operating system opens your app with the URL.
3. Your app checks that the URL is a Genuin link and passes it to the SDK.
4. The SDK asks Genuin which video, community, group, or profile the slug names, then opens that screen over your app.
5. On a device without your app, the link opens your hosted social experience in the browser.

## Before you begin

| Requirement | Detail |
| --- | --- |
| UI module | The screens a link opens live in the UI module: com.begenuin:ui on Android, and GenuinUI on iOS, initialized with GenuinUI.shared.initialize(). With Core alone, the SDK accepts the link and opens nothing. The React Native and Flutter packages include it. |
| Initialized SDK | Initialize the SDK before you hand over a link. On Android, handleDeepLink logs initSDK is not called yet and returns until initSDK has run. |
| Your domain | Links use https://{{SUBDOMAIN}}.begenuin.com unless you white-label your social experience under your own domain in Brand Control Center (BCC), under Build > Publish Brand Community > White label Settings. |
| App identifiers | Your Android package name and signing certificate fingerprints, and your Apple Team ID and bundle ID, for Step 1. |

## Link formats

Genuin builds share links from your hosted social experience's base URL and the item's slug. The base URL is your white-labelled domain, or `https://{{SUBDOMAIN}}.begenuin.com` when your social experience isn't white-labelled.

```text title="Share links"
https://{{YOUR_DOMAIN}}/video/{{VIDEO_SLUG}}?community={{COMMUNITY_SHARE_STRING}}&loop={{GROUP_SHARE_STRING}}
https://{{YOUR_DOMAIN}}/community/{{COMMUNITY_SLUG}}
https://{{YOUR_DOMAIN}}/loop/{{GROUP_SLUG}}?community={{COMMUNITY_SHARE_STRING}}
https://{{YOUR_DOMAIN}}/profile/{{NICKNAME}}
```

When a signed-in member shares from the Android or iOS SDK, the link also carries `utm_source`, `utm_campaign`, and `from_username`. The Web SDK adds `utm_source=web`. The SDKs record these parameters for attribution and route on the first path segment:

| Path | What the SDK opens | action values it honours |
| --- | --- | --- |
| /video/{slug} | The video, playing in its group's full-screen feed | comment opens the comments, repost opens the repost flow, report opens the report flow |
| /community/{slug} | The community's detail screen | join starts the join flow |
| /group/{slug} or /loop/{slug} | The group's detail screen | subscribe subscribes the signed-in member |
| /profile/{nickname} | The member's profile | none |
| /brand/{slug} | Your brand's profile. Invitation links use this path with action=invite; pass them unchanged. | invite, upgrade |
| /dlk/{id} | A short link. The SDK asks Genuin for the full URL, then routes that URL by this table. | none |
| /notification | The member's notifications screen | none |

The video slug comes from the share link. A slug that names a deleted or private item doesn't open anything. The Android SDK shows Genuin's error message as a toast, and the iOS SDK shows it in an alert.

This page covers links coming into your app. Links that Genuin views open when a viewer interacts, such as `isDirectDeepLinkEnabled`, are covered in [Customize the Genuin video player](https://begenuin.com/developers/browse-by-products/control/how-does-a-brand-customize-the-genuin-video-player-controls).

> **Caution** A link with a token query parameter carries a Genuin access token. When no member is signed in on the device, the SDK signs in with that token. Embeds with isDirectDeepLinkWithLoginEnabled add it to the links they open, so treat those links as credentials and don't log or store them.

## Step 1: Register your app with Genuin

A white-labelled domain is a CNAME record that sends every request to Genuin, so Genuin serves the two association files on it. These are `/.well-known/assetlinks.json` for Android and `/.well-known/apple-app-site-association` for iOS. Genuin builds both from your brand's configuration, and Brand Control Center has no screen for them.

1. Send your Genuin contact the domain, your Android package name, and the SHA-256 fingerprint of every certificate that signs your app. Include the Google Play app signing key, because Play re-signs the APKs it delivers.
2. Send your Apple Team ID and bundle ID in the same message.
3. When Genuin confirms the change, fetch both files from your domain:

```bash title="Check the association files"
curl https://{{YOUR_DOMAIN}}/.well-known/assetlinks.json
curl https://{{YOUR_DOMAIN}}/.well-known/apple-app-site-association
```

A brand without the configuration returns `[]` for the first file and `{"applinks":{"apps":[],"details":[]}}` for the second, both with status `200`. A configured Android entry has this shape:

```json title="assetlinks.json"
[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "{{YOUR_PACKAGE_NAME}}",
    "sha256_cert_fingerprints": ["{{YOUR_SIGNING_KEY_SHA256}}"]
  }
}]
```

The iOS file lists your app as `{{TEAM_ID}}.{{BUNDLE_ID}}` under `applinks`. Without these entries, Android and iOS won't verify your app for the domain, and links keep opening in the browser.

## Step 2: Hand links to the SDK on Android

1. Add an intent filter to your launcher activity. Scope it to the Genuin paths, so any other page on the domain still opens in the browser. Use `singleTask` so a link that arrives while the app runs reaches `onNewIntent`.

```xml title="AndroidManifest.xml"
<activity
    android:name=".MainActivity"
    android:exported="true"
    android:launchMode="singleTask">
    <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="{{YOUR_DOMAIN}}" />
        <data android:pathPrefix="/video/" />
        <data android:pathPrefix="/community/" />
        <data android:pathPrefix="/group/" />
        <data android:pathPrefix="/loop/" />
        <data android:pathPrefix="/profile/" />
        <data android:pathPrefix="/brand/" />
        <data android:pathPrefix="/dlk/" />
    </intent-filter>
</activity>
```

2. Check each incoming URL, then pass Genuin links to `GenuinSDK.handleDeepLink`. Call `GenuinSDK.initSDK` first, in your `Application` class.

```kotlin title="MainActivity.kt"
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.begenuin.begenuin.GenuinSDK

private val genuinPaths = setOf("video", "community", "group", "loop", "profile", "brand", "dlk", "notification")

fun Uri.isGenuinLink(): Boolean =
    host == "{{YOUR_DOMAIN}}" && pathSegments.firstOrNull() in genuinPaths

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        routeLink(intent)                 // cold start
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        routeLink(intent)                 // app already running
    }

    private fun routeLink(intent: Intent?) {
        val uri = intent?.data ?: return
        if (uri.isGenuinLink()) GenuinSDK.handleDeepLink(this, intent) // else: your own routing
    }
}
```

The Android SDK has no method that checks a URL first. `handleDeepLink` starts the SDK's link activity, which loads your brand configuration if needed, resolves the slug, opens the screen, and closes. For a path it doesn't recognize, the activity closes without opening anything, and it never falls back to the browser. That's why the check runs before the call.

3. To open a Genuin URL that reached your app another way, such as a QR code or your own push payload, wrap it in an intent:

```kotlin title="Open a Genuin URL from your code"
val link = Uri.parse("https://{{YOUR_DOMAIN}}/video/{{VIDEO_SLUG}}")
GenuinSDK.handleDeepLink(context, Intent(Intent.ACTION_VIEW, link))
```

Any `Context` works. When it isn't an `Activity`, the SDK adds `FLAG_ACTIVITY_NEW_TASK` to the intent it starts.

## Step 3: Hand links to the SDK on iOS

1. In Xcode, open your target's **Signing & Capabilities** tab, add **Associated Domains**, and enter `applinks:{{YOUR_DOMAIN}}`.
2. Ask the SDK whether it handles each universal link, then open it on a view controller:

```swift title="SceneDelegate.swift"
import UIKit
import GenuinCore

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
               options connectionOptions: UIScene.ConnectionOptions) {
        // Set up your window here, then:
        route(connectionOptions.userActivities.first?.webpageURL)    // cold start
    }

    func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
        route(userActivity.webpageURL)                                // app already running
    }

    private func route(_ url: URL?) {
        guard let url, url.host == "{{YOUR_DOMAIN}}" else { return }
        if GenuinSDK.shared.willHandleDeepLink(url: url), let root = window?.rootViewController {
            DispatchQueue.main.async { GenuinSDK.shared.handleDeeplink(viewController: root) }
        }
        // Otherwise: your own routing
    }
}
```

`willHandleDeepLink(url:)` returns `true` when any path component of the URL is a Genuin link type, and it stores the URL. `handleDeeplink(viewController:)` shows a progress indicator and fetches your brand details. It then opens the stored link 0.3 seconds later, presenting from the view controller you pass.

The URL stays stored until a `handleDeeplink` call consumes it, so you can wait until your home screen appears and call it from that screen's `viewDidAppear`. In an app without scenes, make the same calls from `application(_:continue:restorationHandler:)` in your `AppDelegate`.

`handleDeeplink(completion:)` resolves the link without presenting anything, so use the view-controller variant to show content. `willHandleDeepLink` also returns `true` for a few paths the Genuin app uses, such as `terms` and `privacy`. The host check in the sample keeps links to other domains away from the SDK.

## Step 4: Hand links to the SDK in React Native

On Android, use the same intent filter and check as [Step 2](#step-2-hand-links-to-the-sdk-on-android), and call the package's wrapper after `GenuinSDKPluginModule.initSDK`:

```kotlin title="MainActivity.kt (React Native)"
import com.genuinsdk.GenuinSDKPluginModule

private fun routeLink(intent: Intent?) {
    val uri = intent?.data ?: return
    if (uri.isGenuinLink()) GenuinSDKPluginModule.handleDeepLink(this, intent)
}
```

React Native receives every intent through `ReactActivity`, so links you don't pass to Genuin still reach the `Linking` API.

On iOS, use the Objective-C wrapper from [Integrate the Genuin SDK with React Native apps](https://begenuin.com/developers/developer-resources/sdk/how-do-you-integrate-the-genuin-react-native-sdk-into-your-application), and hand everything else to React Native:

```swift title="AppDelegate.swift"
override func application(_ application: UIApplication, continue userActivity: NSUserActivity,
                          restorationHandler: @escaping ([any UIUserActivityRestoring]?) -> Void) -> Bool {
  if let url = userActivity.webpageURL,
     GenuinModuleObjCWrapper.willHandleDeepLink(with: url),
     GenuinModuleObjCWrapper.handleDeepLink(url, controller: window.rootViewController) {
    return true
  }
  return RCTLinkingManager.application(application, continue: userActivity,
                                       restorationHandler: restorationHandler)
}
```

`handleDeepLink(_:controller:)` returns `true` for a Genuin link and opens it 0.3 seconds later on the controller you pass. With a `nil` controller, it returns `true` and opens nothing. The package exposes no deep-link function to JavaScript.

## Step 5: Hand links to the SDK in Flutter

genuin_sdk 1.0.1 forwards links to the native SDK it wraps, version 1.1.9.x. Declare the same Android intent filter and iOS Associated Domains entry as in Steps 2 and 3.

1. On Android, pass the intent from your `FlutterFragmentActivity`:

```kotlin title="android/app/src/main/kotlin/{{YOUR_PACKAGE}}/MainActivity.kt"
override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    if (intent.data?.isGenuinLink() == true) GenuinSdkPlugin.handleDeepLink(this, intent)
}
```

Make the same call in `onCreate`, after `GenuinSdkPlugin.initSDK`.

2. On iOS, forward the user activity, then let the plugin open the URL:

```swift title="ios/Runner/AppDelegate.swift"
override func application(_ application: UIApplication, continue userActivity: NSUserActivity,
                          restorationHandler: @escaping ([any UIUserActivityRestoring]?) -> Void) -> Bool {
  let forwarded = GenuinSdkPlugin.flutterapplication(application, continue: userActivity,
                                                     restorationHandler: restorationHandler)
  if GenuinSdkPlugin.handleDeepLink(dlURL: userActivity.webpageURL, controller: window?.rootViewController) {
    return true
  }
  return forwarded
}
```

`handleDeepLink(dlURL:controller:)` returns `true` when the native SDK recognizes the URL, then opens it 0.3 seconds later. The root view controller must be the navigation controller from `GenuinSdkPlugin.getGenuinNavigationController`, as set up in [Initialize the Genuin SDK and load a feed in Flutter](https://begenuin.com/developers/developer-resources/sdk/flutter-sdk-developer-reference-how-do-i-initialize-the-genuin-sdk-and-load-a-feed). There's no Dart API for links.

## Step 6: Handle share links on your website

The Web SDK builds share links the same way. Its share button joins your hosted social experience's base URL to the video's path and adds `utm_source=web`. On a desktop browser it copies the link and shows **Link Copied**. On phones and tablets it opens the system share sheet when the browser supports one, and copies the link otherwise.

> **Note** If the URL of your page contains the text embed, the share button on a phone opens the link in a new tab instead of the share sheet.

A shared link opens your hosted social experience, not the page that hosts your embed. On a phone with your app verified for the domain, it opens your app instead. The Web SDK doesn't read share links from your page's URL. To open a specific video on your own page, map your own query parameter to `start_video_slug`:

```html title="Open a shared video on your page"
<div id="gen-sdk" class="gen-sdk-class" style="width: 100%; height: 600px;"></div>
<script src="https://media.begenuin.com/sdk/2.0.6/gen_sdk.min.js"></script>
<script>
  const slug = new URLSearchParams(window.location.search).get("video");
  window.genuin.init({
    embed_id: "{{EMBED_ID}}",
    api_key: "<<YOUR_API_KEY>>",
    ...(slug ? { start_video_slug: slug } : {}),
  });
  window.genuin.on("onVideoNotFound", (event) => {
    console.warn("Shared video unavailable:", event.payload.slug, event.payload.errorCode);
  });
</script>
```

`onVideoNotFound` fires with `errorCode` `5209` when no video has that slug, and with `NO_FEEDS_RETURNED` when the lookup returns nothing. The `action` and `comment_id` options that open comments or other interactions on the start video are listed in [Embed the Genuin Web SDK on your website](https://begenuin.com/developers/developer-resources/sdk/how-do-you-embed-and-configure-the-genuin-web-sdk-on-your-website).

## Step 7: Test your links

Use a real share link from your social experience, so the slug exists. On Android 12 and later, re-run verification and then open the link:

```bash title="Test on Android"
adb shell pm verify-app-links --re-verify {{YOUR_PACKAGE_NAME}}
adb shell pm get-app-links {{YOUR_PACKAGE_NAME}}
adb shell am start -W -a android.intent.action.VIEW -c android.intent.category.BROWSABLE \
  -d "https://{{YOUR_DOMAIN}}/video/{{VIDEO_SLUG}}"
```

`get-app-links` must report your domain as `verified`. On iOS, run `xcrun simctl openurl booted "https://{{YOUR_DOMAIN}}/video/{{VIDEO_SLUG}}"` against a simulator. On a device, tap the link in Notes or Messages, because Safari opens a link typed in its address bar as a web page.

| Symptom | Cause | Fix |
| --- | --- | --- |
| The link opens in the browser | Genuin's association file doesn't list your app yet, or the device fetched the file before Genuin updated it | Fetch the files as in Step 1, then reinstall the app so the system checks the domain again. |
| Android opens your app, then nothing happens | The UI module is missing, initSDK hasn't run, or the path isn't in the link table | Add com.begenuin:ui, initialize in Application.onCreate, and send only Genuin paths. |
| iOS opens your app, then nothing happens | GenuinUI.shared.initialize() wasn't called, or handleDeeplink got no view controller | Initialize GenuinUI before GenuinSDK, and pass a view controller that's in the window. |
| A toast or alert shows an error | The video, community, or group was deleted, made private, or never existed | Expected. Check the slug in Brand Control Center. |
| A link opens twice | Both your router and the SDK handled it | Return after passing the URL to the SDK. |

## Limitations

- **No check method on Android**: `handleDeepLink` accepts any intent. Filter by host and first path segment yourself.
- **HTTPS links only**: The SDKs route on the URL path, so custom schemes such as `myapp://video/{{VIDEO_SLUG}}` aren't supported.
- **Association files through Genuin**: Genuin serves both files from your brand's configuration. Changes go through your Genuin contact.
- **UI module required**: Core alone accepts links and opens nothing.
- **Network required**: Every link costs a lookup to resolve the slug. Short `/dlk/` links cost a second lookup.
- **Web pages don't route share links**: The Web SDK opens a shared video only when you pass `start_video_slug`.
- **Flutter uses native SDK 1.1.9.x**: The link table is verified against SDK 2.1.5, which Android, iOS, and React Native use.

## See also

- [Integrate the Genuin Android SDK](https://begenuin.com/developers/developer-resources/sdk/how-to-integrate-and-power-your-brand-community-in-android-apps)
- [Integrate the Genuin iOS SDK](https://begenuin.com/developers/developer-resources/sdk/developer/genuin-sdk/native-compatibility/ios/how-to-integrate-and-power-your-brand-community-in-ios-apps)
- [Integrate the Genuin SDK with React Native apps](https://begenuin.com/developers/developer-resources/sdk/how-do-you-integrate-the-genuin-react-native-sdk-into-your-application)
- [Initialize the Genuin SDK and load a feed in Flutter](https://begenuin.com/developers/developer-resources/sdk/flutter-sdk-developer-reference-how-do-i-initialize-the-genuin-sdk-and-load-a-feed)
- [Embed the Genuin Web SDK on your website](https://begenuin.com/developers/developer-resources/sdk/how-do-you-embed-and-configure-the-genuin-web-sdk-on-your-website)
- [Sign members out and handle expired sessions](https://begenuin.com/developers/developer-resources/sdk/sign-members-out-and-handle-expired-sessions)
