Skip to main content

Deep Linking

Deep linking allows users to open specific StorifyMe stories directly from shared links, providing a seamless experience when content is shared across platforms.

StorifyMe provides two approaches for implementing deep links:

StorifyMe automatically generates and hosts the Digital Asset Links file for you. This is the simplest approach and requires no backend infrastructure on your end.

Prerequisites

  • Android app with StorifyMe SDK integrated
  • StorifyMe account
  • App published to Google Play Store (or available for testing)

Step 1: Configure App Credentials in Dashboard

  1. Log in to your StorifyMe dashboard

  2. Navigate to Settings → Mobile

  3. In the Android section, enter:

    • Package Name: Your app's package name (e.g., com.yourcompany.app)
    • SHA-256 Fingerprints: Your app signing certificate fingerprints (see below)
  4. Click Save

Getting SHA-256 Fingerprints

You need to provide SHA-256 fingerprints for all signing keys (debug, release, and Play App Signing if applicable).

Debug Keystore:

keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android

Release Keystore:

keytool -list -v -keystore /path/to/your/keystore.jks -alias your-key-alias

Play App Signing Key:

  1. Go to Google Play Console
  2. Select your app → Setup → App integrity
  3. Copy the SHA-256 fingerprint under "App signing key certificate"

Add ALL fingerprints to the StorifyMe dashboard (separated by commas or new lines).

StorifyMe will automatically generate the Asset Links file at:

  • https://storifyme.xyz/.well-known/assetlinks.json
  • https://storifyme.com/.well-known/assetlinks.json

Step 2: Add Intent Filters to AndroidManifest.xml

Open your AndroidManifest.xml and add intent filters to handle deep links:

AndroidManifest.xml
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask">

<!-- Your existing intent filters -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<!-- StorifyMe Deep Links - storifyme.xyz -->
<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="storifyme.xyz"
android:pathPrefix="/stories/YOUR_ACCOUNT_ID/" />
</intent-filter>

<!-- StorifyMe Deep Links - storifyme.com -->
<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="storifyme.com"
android:pathPrefix="/stories/YOUR_ACCOUNT_ID/" />
</intent-filter>

<!-- Support for /shorts/ path -->
<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="storifyme.xyz"
android:pathPrefix="/shorts/YOUR_ACCOUNT_ID/" />
</intent-filter>

<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="storifyme.com"
android:pathPrefix="/shorts/YOUR_ACCOUNT_ID/" />
</intent-filter>

</activity>

Important: Replace YOUR_ACCOUNT_ID with your actual StorifyMe account ID. The pathPrefix ensures your app only receives links for your account (multi-tenant isolation).

The deep link URL format is:

https://storifyme.xyz/stories/{ACCOUNT_ID}/{story_handle}
https://storifyme.xyz/shorts/{ACCOUNT_ID}/{story_handle}

Kotlin Implementation

Update your Activity (e.g., MainActivity.kt):

MainActivity.kt
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.storify.android_sdk.PreviewStoryByHandleLauncher

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// Handle deep link on app launch
handleDeepLink(intent)
}

override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
// Handle deep link when app is already running
intent?.let { handleDeepLink(it) }
}

private fun handleDeepLink(intent: Intent) {
val uri = intent.data
if (uri != null && uri.scheme == "https") {
val handle = extractStoryHandle(uri)
if (handle != null) {
// Launch story directly
PreviewStoryByHandleLauncher.INSTANCE.launch(this, handle)
}
}
}

private fun extractStoryHandle(uri: Uri): String? {
val pathSegments = uri.pathSegments

// For StorifyMe-hosted URLs: https://storifyme.xyz/stories/{ACCOUNT_ID}/{story_handle}
return when {
pathSegments.size >= 3 &&
(pathSegments[0] == "stories" || pathSegments[0] == "shorts") ->
pathSegments[2]

else -> null
}
}
}

Java Implementation

import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import com.storify.android_sdk.PreviewStoryByHandleLauncher;
import java.util.List;

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// Handle deep link on app launch
handleDeepLink(getIntent());
}

@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
// Handle deep link when app is already running
handleDeepLink(intent);
}

private void handleDeepLink(Intent intent) {
Uri data = intent.getData();

if (data != null && "https".equals(data.getScheme())) {
String handle = extractStoryHandle(data);
if (handle != null) {
// Launch story directly
PreviewStoryByHandleLauncher.INSTANCE.launch(this, handle);
}
}
}

private String extractStoryHandle(Uri uri) {
List<String> pathSegments = uri.getPathSegments();

// For StorifyMe-hosted URLs: https://storifyme.xyz/stories/{ACCOUNT_ID}/{story_handle}
if (pathSegments.size() >= 3 &&
("stories".equals(pathSegments.get(0)) || "shorts".equals(pathSegments.get(0)))) {
return pathSegments.get(2);
}

return null;
}
}

Using ADB

# Test with storifyme.xyz
adb shell am start -W -a android.intent.action.VIEW \
-d "https://storifyme.xyz/stories/YOUR_ACCOUNT_ID/test-story" \
com.yourcompany.app

# Test with storifyme.com
adb shell am start -W -a android.intent.action.VIEW \
-d "https://storifyme.com/stories/YOUR_ACCOUNT_ID/test-story" \
com.yourcompany.app

# Test shorts path
adb shell am start -W -a android.intent.action.VIEW \
-d "https://storifyme.xyz/shorts/YOUR_ACCOUNT_ID/test-story" \
com.yourcompany.app

Replace:

  • YOUR_ACCOUNT_ID with your StorifyMe account ID
  • com.yourcompany.app with your app's package name
  • test-story with an actual story handle from your account
  1. Send yourself a message (Gmail, Slack, etc.) with a story link:

    https://storifyme.xyz/stories/YOUR_ACCOUNT_ID/test-story
  2. Tap the link on your Android device

  3. Your app should open and display the story

App doesn't open from links:

  • Verify android:autoVerify="true" is set in intent filters
  • Check that path prefix includes your ACCOUNT_ID
  • Verify SHA-256 fingerprints in dashboard are correct
  • Clear app defaults: Settings → Apps → Your App → Open by default → Clear defaults

Check verification status:

adb shell pm get-app-links com.yourcompany.app

Verify Asset Links file:

curl https://storifyme.xyz/.well-known/assetlinks.json | grep "com.yourcompany.app"

Before you begin

Make sure you have configured your sharing domain in the StorifyMe Dashboard Settings. Set either Custom Domain OR Proxy - this domain will be used when links are copied/shared from your stories.

If you want to use your own domain for deep links (e.g., app.yourcompany.com), you'll need to host the Asset Links file on your own server.

1. Update AndroidManifest.xml

Add an intent filter to your main activity (or dedicated deep link activity) to handle incoming links:

AndroidManifest.xml
<activity android:name=".MainActivity">
<!-- Your existing intent filters -->

<!-- Deep link intent filter -->
<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_PROXY_DOMAIN"
android:pathPrefix="/" />
</intent-filter>
</activity>

Replace YOUR_PROXY_DOMAIN with your custom domain.

Create an assetlinks.json file in your backend at https://YOUR_PROXY_DOMAIN/.well-known/assetlinks.json:

assetlinks.json
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.YOUR_PACKAGE",
"sha256_cert_fingerprints": [
"YOUR_PACKAGE_FINGERPRINT"
]
}
}
]
Getting your SHA256 fingerprint

You can get your app's SHA256 fingerprint using:

keytool -list -v -keystore your-release-key.keystore -alias your-key-alias

3. Implementation for Custom Domain

For custom domains, update the URL parsing logic to match your domain structure:

Method 1: Direct Story Launch (Recommended)

For shared story links, use PreviewStoryByHandleLauncher to open stories directly without depending on widget loading:

MainActivity.kt
import com.storify.android_sdk.PreviewStoryByHandleLauncher

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// Handle deep link on app launch
handleDeepLink(intent)
}

override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
// Handle deep link when app is already running
intent?.let { handleDeepLink(it) }
}

private fun handleDeepLink(intent: Intent) {
val uri = intent.data
if (uri != null && uri.scheme == "https") {
val handle = extractStoryHandle(uri)
if (handle != null) {
// Launch story directly
PreviewStoryByHandleLauncher.INSTANCE.launch(this, handle)
}
}
}

private fun extractStoryHandle(uri: Uri): String? {
return when {
// For URLs like: https://domain.com/story-handle
uri.pathSegments.size == 1 -> uri.pathSegments[0]

// For URLs like: https://domain.com/story/story-handle
uri.pathSegments.size >= 2 && uri.pathSegments[0] == "story" ->
uri.pathSegments[1]

// For URLs like: https://domain.com/stories/story-handle
uri.pathSegments.size >= 2 && uri.pathSegments[0] == "stories" ->
uri.pathSegments[1]

else -> null
}
}
}

Method 2: Widget-Based Launch

If you need to open a story within a loaded widget context:

MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var storiesView: StoriesView
private var pendingHandle: String? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

storiesView = findViewById(R.id.storiesView)
setupStoriesView()

// Handle deep link
handleDeepLink(intent)
}

private fun setupStoriesView() {
StorifyMe.instance.eventListener = object : StorifyMeEventListener() {
override fun onLoad(widgetId: Long, stories: List<StoryWithSeen>) {
super.onLoad(widgetId, stories)

// If we have a pending handle, open it now
pendingHandle?.let { handle ->
storiesView.openWidgetStoryByHandle(handle) { result ->
when (result) {
is StorifyMeWidgetStoryNavigatorExecutionResult.Success -> {
// Story opened successfully
}
is StorifyMeWidgetStoryNavigatorExecutionResult.StoryNotFound -> {
// Handle not found in this widget
// Consider using PreviewStoryByHandleLauncher as fallback
PreviewStoryByHandleLauncher.INSTANCE.launch(this@MainActivity, handle)
}
is StorifyMeWidgetStoryNavigatorExecutionResult.WidgetNotLoaded -> {
// Widget not ready yet
}
}
}
pendingHandle = null
}
}

override fun onFail(widgetId: Long, error: String) {
super.onFail(widgetId, error)
// If widget fails to load but we have a handle, try direct launch
pendingHandle?.let { handle ->
PreviewStoryByHandleLauncher.INSTANCE.launch(this@MainActivity, handle)
pendingHandle = null
}
}
}

storiesView.widgetId = YOUR_WIDGET_ID
storiesView.load()
}

private fun handleDeepLink(intent: Intent) {
val uri = intent.data
if (uri != null) {
val handle = extractStoryHandle(uri)
if (handle != null) {
if (storiesView.isLoaded()) {
// Widget is already loaded, open immediately
storiesView.openWidgetStoryByHandle(handle)
} else {
// Store handle to open after widget loads
pendingHandle = handle
}
}
}
}
}

URL Pattern Examples

Your implementation should handle various URL patterns depending on your backend setup:

URL PatternHandle Extraction
https://domain.com/story-handlepathSegments[0]
https://domain.com/story/story-handlepathSegments[1]
https://domain.com/stories/story-handlepathSegments[1]
https://domain.com/content/story-handlepathSegments[1]

Using ADB

Test your deep link implementation using ADB:

adb shell am start \
-W -a android.intent.action.VIEW \
-d "https://YOUR_PROXY_DOMAIN/your-story-handle" \
com.your.package

Using Intent Filter Testing

You can also test using Android's intent filter verification:

adb shell am start \
-a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d "https://YOUR_PROXY_DOMAIN/your-story-handle"

Best Practices

  1. Use Direct Launch for Shared Content: PreviewStoryByHandleLauncher is more reliable for shared links as it doesn't depend on widget loading
  2. Implement Fallback Logic: If a story isn't found in a widget, fall back to direct launch
  3. Handle Multiple URL Patterns: Make your URL parsing robust to handle different backend configurations
  4. Validate Handles: Check if handles exist before attempting to open stories
  5. User Experience: Show loading states while processing deep links
  6. Error Handling: Gracefully handle invalid or expired story handles

Common Issues

  • Verify your assetlinks.json is accessible and properly formatted
  • Check that your app's SHA256 fingerprint matches the one in assetlinks.json
  • Ensure the domain in AndroidManifest.xml matches your proxy domain exactly

Story Not Opening

  • Verify the story handle exists and is published
  • Check if the story is available in the widget you're trying to open it from
  • Use direct launch (PreviewStoryByHandleLauncher) as a fallback

URL Parsing Issues

  • Test with different URL patterns your backend might generate
  • Add logging to debug which path segments contain the handle
  • Handle edge cases like URLs with query parameters