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 community'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 community 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 community site in the browser.

Before you begin

RequirementDetail
UI moduleThe 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 SDKInitialize the SDK before you hand over a link. On Android, handleDeepLink logs initSDK is not called yet and returns until initSDK has run.
Community domainLinks use https://{{SUBDOMAIN}}.begenuin.com unless you white-label the community under your own domain in Brand Control Center, under Build > Publish Brand Community > White label Settings.
App identifiersYour Android package name and signing certificate fingerprints, and your Apple Team ID and bundle ID, for Step 1.

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

Share links
text
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:

PathWhat the SDK opensaction values it honours
/video/{slug}The video, playing in its group's full-screen feedcomment opens the comments, repost opens the repost flow, report opens the report flow
/community/{slug}The community's detail screenjoin starts the join flow
/group/{slug} or /loop/{slug}The group's detail screensubscribe subscribes the signed-in member
/profile/{nickname}The member's profilenone
/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
/notificationThe member's notifications screennone

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.

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:
Check the association files
bash
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:

assetlinks.json
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.

  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.
AndroidManifest.xml
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>
  1. Check each incoming URL, then pass Genuin links to GenuinSDK.handleDeepLink. Call GenuinSDK.initSDK first, in your Application class.
MainActivity.kt
kotlin
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.

  1. 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:
Open a Genuin URL from your code
kotlin
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.

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

On Android, use the same intent filter and check as Step 2, and call the package's wrapper after GenuinSDKPluginModule.initSDK:

MainActivity.kt (React Native)
kotlin
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, and hand everything else to React Native:

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

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:
android/app/src/main/kotlin/{{YOUR_PACKAGE}}/MainActivity.kt
kotlin
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.

  1. On iOS, forward the user activity, then let the plugin open the URL:
ios/Runner/AppDelegate.swift
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. There's no Dart API for links.

The Web SDK builds share links the same way. Its share button joins your community'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 Genuin community site, 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:

Open a shared video on your page
html
<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.

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

Test on Android
bash
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.

SymptomCauseFix
The link opens in the browserGenuin's association file doesn't list your app yet, or the device fetched the file before Genuin updated itFetch the files as in Step 1, then reinstall the app so the system checks the domain again.
Android opens your app, then nothing happensThe UI module is missing, initSDK hasn't run, or the path isn't in the link tableAdd com.begenuin:ui, initialize in Application.onCreate, and send only Genuin paths.
iOS opens your app, then nothing happensGenuinUI.shared.initialize() wasn't called, or handleDeeplink got no view controllerInitialize GenuinUI before GenuinSDK, and pass a view controller that's in the window.
A toast or alert shows an errorThe video, community, or group was deleted, made private, or never existedExpected. Check the slug in Brand Control Center.
A link opens twiceBoth your router and the SDK handled itReturn 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

TRUST AND COMPLIANCE

© 2026 Genuin Inc.

Genuin Footer