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:
- A member taps Share on a video. The link uses your community domain, such as
https://community.example.com/video/{{VIDEO_SLUG}}. - 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.
- Your app checks that the URL is a Genuin link and passes it to the SDK.
- The SDK asks Genuin which video, community, group, or profile the slug names, then opens that screen over your app.
- On a device without your app, the link opens your community site 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. |
| Community domain | Links 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 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 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.
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.
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.
- 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.
- Send your Apple Team ID and bundle ID in the same message.
- When Genuin confirms the change, fetch both files from your domain:
curl https://{{YOUR_DOMAIN}}/.well-known/assetlinks.json
curl https://{{YOUR_DOMAIN}}/.well-known/apple-app-site-associationA 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:
[{
"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
- 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
singleTaskso a link that arrives while the app runs reachesonNewIntent.
<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>- Check each incoming URL, then pass Genuin links to
GenuinSDK.handleDeepLink. CallGenuinSDK.initSDKfirst, in yourApplicationclass.
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.
- 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:
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
- In Xcode, open your target's Signing & Capabilities tab, add Associated Domains, and enter
applinks:{{YOUR_DOMAIN}}. - Ask the SDK whether it handles each universal link, then open it on a view controller:
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, and call the package's wrapper after GenuinSDKPluginModule.initSDK:
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:
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.
- On Android, pass the intent from your
FlutterFragmentActivity:
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.
- On iOS, forward the user activity, then let the plugin open the URL:
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.
Step 6: Handle share links on your website
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:
<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.
Step 7: Test your links
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:
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:
handleDeepLinkaccepts 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.

