Blueshift universal links (Android)

Set up universal links to open your Android app directly from emails or web messages.

Blueshift universal links enable users to navigate directly from emails or web messages to specific screens within your Android app. When a user taps a universal link, Android launches your app instead of opening a browser.

Prerequisites

Before setting up universal links, ensure you have completed:

  • SDK setup
  • CNAME domain configuration (contact the Blueshift implementation or support team)

Setup

Step 1: Generate SHA-256 certificate fingerprint

Your SHA-256 fingerprint is required to create the assetlinks.json file.

To get your SHA-256 certificate fingerprint, run this command in your terminal:

keytool -keystore <your_keystore_path> -list -v

Copy the SHA-256 value from the output. It will look like:

SHA-256: AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90

Create the assetlinks.json file:

Create a file named assetlinks.json with this content:

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.yourapp",
    "sha256_cert_fingerprints": [
      "AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90"
    ]
  }
}]

Replace:

  • com.example.yourapp with your actual package name
  • The fingerprint value with your SHA-256 from the keytool command

Note: Keep the colon-separated format for the fingerprint as shown above.

For multiple signing keys (debug and release)

If you have separate debug and release signing keys, include both fingerprints:

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.yourapp",
    "sha256_cert_fingerprints": [
      "DEBUG_KEY_FINGERPRINT_HERE",
      "RELEASE_KEY_FINGERPRINT_HERE"
    ]
  }
}]

Step 2: Submit assetlinks.json to Blueshift

  1. Sign in to Blueshift.
  2. Navigate to the top-right corner, click the hamburger menu (☰), and select Account Settings.
  3. Select the Other Settings tab.
  4. In the Asset Links File field, paste the contents of your assetlinks.json file.

The file should include:

  • Your app's package name
  • Your SHA-256 certificate fingerprint

For more information, see Declare website associations on the Android developer portal.

Step 3: Configure intent filters

Add intent filters to your activity in AndroidManifest.xml:

<activity android:name=".MainActivity">
    <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="links.clientdomain.com"
            android:pathPrefix="/track" />
        <data 
            android:scheme="https"
            android:host="links.clientdomain.com"
            android:pathPrefix="/z" />
    </intent-filter>
</activity>

Replace links.clientdomain.com with your actual CNAME domain that points to links.getblueshift.com.

📘

Note:

/track handles tracking and analytics links.
/z handles deep links that open specific screens in your app.

If a link isn’t opening the app, make sure the URL path matches one of these and is listed in your AndroidManifest.xml.

Handle deep links

There are two ways to handle universal links in your app.

Option 1: Override BlueshiftLinksActivity

Create an activity that extends BlueshiftLinksActivity:

import com.blueshift.BlueshiftLinksActivity
import com.blueshift.BlueshiftLinksListener
import android.net.Uri
import android.util.Log

class MyLinksActivity : BlueshiftLinksActivity() {
    
    override fun getBlueshiftLinksListener(): BlueshiftLinksListener {
        return object : BlueshiftLinksListener {
            override fun onLinkProcessingStart() {
                Log.d("DeepLink", "Link processing started")
            }
            
            override fun onLinkProcessingComplete(uri: Uri?) {
                Log.d("DeepLink", "Received URI: $uri")
                
                // Navigate based on the URI
                when (uri?.path) {
                    "/products" -> {
                        // Navigate to products screen
                    }
                    "/cart" -> {
                        // Navigate to cart screen
                    }
                }
            }
            
            override fun onLinkProcessingError(e: Exception?, link: Uri?) {
                Log.e("DeepLink", "Error: ${e?.message}")
            }
        }
    }
}
import com.blueshift.BlueshiftLinksActivity;
import com.blueshift.BlueshiftLinksListener;
import android.net.Uri;
import android.util.Log;

public class MyLinksActivity extends BlueshiftLinksActivity {
    
    @Override
    protected BlueshiftLinksListener getBlueshiftLinksListener() {
        return new BlueshiftLinksListener() {
            @Override
            public void onLinkProcessingStart() {
                Log.d("DeepLink", "Link processing started");
            }
            
            @Override
            public void onLinkProcessingComplete(Uri uri) {
                Log.d("DeepLink", "Received URI: " + uri);
            }
            
            @Override
            public void onLinkProcessingError(Exception e, Uri link) {
                Log.e("DeepLink", "Error: " + e.getMessage());
            }
        };
    }
}

Register this activity in AndroidManifest.xml:

<activity 
    android:name=".MyLinksActivity"
    android:exported="true">
    <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="links.clientdomain.com"
            android:pathPrefix="/track" />
        <data 
            android:scheme="https"
            android:host="links.clientdomain.com"
            android:pathPrefix="/z" />
    </intent-filter>
</activity>

Option 2: Use handleBlueshiftUniversalLinks()

Call the SDK's handleBlueshiftUniversalLinks() method from any activity:

import com.blueshift.BlueshiftLinksHandler
import com.blueshift.BlueshiftLinksListener
import android.os.Bundle
import android.net.Uri
import android.util.Log
import androidx.appcompat.app.AppCompatActivity

class MyLinksActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        BlueshiftLinksHandler(this).handleBlueshiftUniversalLinks(
            intent,
            object : BlueshiftLinksListener {
                override fun onLinkProcessingStart() {
                    Log.d("DeepLink", "Link processing started")
                }
                
                override fun onLinkProcessingComplete(uri: Uri?) {
                    Log.d("DeepLink", "Received URI: $uri")
                }
                
                override fun onLinkProcessingError(e: Exception?, link: Uri?) {
                    Log.e("DeepLink", "Error: ${e?.message}")
                }
            }
        )
    }
}
import com.blueshift.BlueshiftLinksHandler;
import com.blueshift.BlueshiftLinksListener;
import android.os.Bundle;
import android.net.Uri;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;

public class MyLinksActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        new BlueshiftLinksHandler(this).handleBlueshiftUniversalLinks(
            getIntent(),
            new BlueshiftLinksListener() {
                @Override
                public void onLinkProcessingStart() {
                    Log.d("DeepLink", "Link processing started");
                }
                
                @Override
                public void onLinkProcessingComplete(Uri uri) {
                    Log.d("DeepLink", "Received URI: " + uri);
                }
                
                @Override
                public void onLinkProcessingError(Exception e, Uri link) {
                    Log.e("DeepLink", "Error: " + e.getMessage());
                }
            }
        );
    }
}

Register this activity in AndroidManifest.xml as shown in Option 1.

Testing universal links

Step 1: Verify intent filter configuration

Build and install your app, then test that your activity receives deep link intents.

Add a custom scheme to your intent filter for local testing:

<activity android:name=".MyLinksActivity">
    <!-- Custom scheme for local testing -->
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        
        <data android:scheme="yourappname" />
    </intent-filter>
    
    <!-- HTTPS intent filter for production -->
    <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="links.clientdomain.com"
            android:pathPrefix="/track" />
    </intent-filter>
</activity>

Test with ADB (Android Debug Bridge - a command-line tool for communicating with Android devices):

adb shell am start -a android.intent.action.VIEW -d "yourappname://test"

Check Logcat:

adb logcat | grep "DeepLink"

Expected output:

D/DeepLink: Link processing started
D/DeepLink: Received URI: yourappname://test

If you see an error like "No redirect URL (redir) found", this is expected for test links. The activity is working correctly - genuine Blueshift links will include the required parameters.

Step 2: Verify assetlinks.json is hosted

Before testing production universal links, confirm Blueshift is hosting your assetlinks.json file.

Open your browser and visit:

https://links.clientdomain.com/.well-known/assetlinks.json

Replace links.clientdomain.com with your CNAME domain. You should see JSON output without requiring authentication.

Step 3: Test with production universal link

Once your CNAME and assetlinks.json are configured:

  1. Create a test campaign in Blueshift with a universal link using your CNAME domain
  2. Send the campaign to your test device email
  3. Open the email on your device
  4. Tap the link

Expected behavior:

  • App launches (or comes to foreground if already open)
  • Your MyLinksActivity processes the link
  • Logcat shows "Link processing started" and "Received URI"

Check Logcat:

adb logcat | grep "DeepLink"

If the link opens in the browser instead of the app:

  • Android may not have verified domain ownership yet
  • Reinstall the app to retry verification
  • Verify assetlinks.json is accessible and correctly formatted

Advanced

Extract query parameters

override fun onLinkProcessingComplete(uri: Uri?) {
    val productId = uri?.getQueryParameter("product_id")
    val category = uri?.getQueryParameter("category")
    
    val intent = Intent(this, ProductDetailActivity::class.java).apply {
        putExtra("PRODUCT_ID", productId)
        putExtra("CATEGORY", category)
    }
    startActivity(intent)
    finish()
}
@Override
public void onLinkProcessingComplete(Uri uri) {
    String productId = uri.getQueryParameter("product_id");
    String category = uri.getQueryParameter("category");
    
    Intent intent = new Intent(this, ProductDetailActivity.class);
    intent.putExtra("PRODUCT_ID", productId);
    intent.putExtra("CATEGORY", category);
    startActivity(intent);
    finish();
}

Custom navigation logic

override fun onLinkProcessingComplete(uri: Uri?) {
    when {
        uri?.path?.startsWith("/products") == true -> {
            startActivity(Intent(this, ProductsActivity::class.java))
        }
        uri?.path?.startsWith("/cart") == true -> {
            startActivity(Intent(this, CartActivity::class.java))
        }
        else -> {
            startActivity(Intent(this, MainActivity::class.java))
        }
    }
    finish()
}
@Override
public void onLinkProcessingComplete(Uri uri) {
    String path = uri.getPath();
    
    if (path != null && path.startsWith("/products")) {
        startActivity(new Intent(this, ProductsActivity.class));
    } else if (path != null && path.startsWith("/cart")) {
        startActivity(new Intent(this, CartActivity.class));
    } else {
        startActivity(new Intent(this, MainActivity.class));
    }
    
    finish();
}

Troubleshooting

Intent filter test fails (Step 1):

  • Verify android:exported="true" is set on your activity
  • Check that the activity is registered in AndroidManifest.xml
  • Rebuild and reinstall the app

assetlinks.json not accessible (Step 2):

  • Contact your Blueshift implementation team to verify CNAME setup
  • Confirm you submitted the correct assetlinks.json content to Blueshift

Link opens in browser instead of app (Step 3):

  • Android requires domain verification before routing links to apps
  • Uninstall and reinstall the app to retry verification
  • Verify assetlinks.json includes the correct package name and SHA-256 fingerprint
  • Check that the CNAME domain correctly points to links.getblueshift.com

Existing app links stopped working:

Reference documentation