App Links setup
Claim your Lander domain with a verified App Links intent filter, register your signing fingerprints on the Platforms page, add an optional custom scheme and hand intents to the SDK.
App Links open your app instead of the browser when two things agree: your manifest says the app handles a domain (an intent filter with android:autoVerify="true"), and the domain says it trusts your app's signing key (the assetlinks.json file). Lander hosts the file for you. You add the intent filter and tell Lander your fingerprints.
Know your link domain first: open Build > Domains in the dashboard. Hosted domains are named after your project and organization, for example acme-shop-acme.golander.link. The examples below use acme.golander.link, the domain of Lander's demo project, and the package name com.acme.shop.
Collect your SHA-256 fingerprints
Android only trusts assetlinks.json for the exact keys that signed the installed APK. You almost always have more than one:
| Key | Signs | How to get its SHA-256 |
|---|---|---|
| Debug key | Builds you run from Android Studio | keytool on ~/.android/debug.keystore (below) |
| Upload key | The bundle you upload to Play | keytool on your upload keystore |
| Play App Signing key | What users install from Google Play | Play Console, on your app's App signing page (copy the app signing key certificate) |
# Debug key (default password is "android")
keytool -list -v -keystore ~/.android/debug.keystore \
-alias androiddebugkey -storepass android -keypass android
# Or let Gradle print every variant's signing config
./gradlew signingReportCopy the SHA256: lines. They look like AA:BB:CC:... (32 pairs).
The Play App Signing key is the one users get
If you use Play App Signing, Google re-signs your app. An app installed from the Play Store carries Google's key, not your upload key, so App Links only verify for store installs when that fingerprint is registered.
Add your Android app on the Platforms page
Open Build > Platforms in the dashboard, switch to the Android tab and fill in the form:
| Field | Value |
|---|---|
| Package name | Your applicationId, for example com.acme.shop. Each flavor with its own suffix (com.acme.shop.dev) is a different app. |
| SHA-256 certificate fingerprints | One row per key from step 1. Colon separated or 64 bare hex characters both work. |
| Play Store URL (optional) | Used to send people who do not have the app to the store. Lander builds one from the package name when empty. |
| URI scheme (optional) | Your custom scheme without ://, for example acmeshop. |
Save. The preview on the right shows the file Lander now serves on your link domain:
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.acme.shop",
"sha256_cert_fingerprints": ["AA:BB:..."]
}
}
]Check it is live:
curl -i https://acme.golander.link/.well-known/assetlinks.jsonYou should get 200. A 404 means no Android app with at least one fingerprint is saved for this domain. More detail is in Dashboard: Platforms.
Add the intent filters
In app/src/main/AndroidManifest.xml, on the activity that should open links (usually your launcher activity), add:
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- App Links for your Lander domain. autoVerify makes Android check
https://acme.golander.link/.well-known/assetlinks.json at install time. -->
<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="acme.golander.link" />
</intent-filter>
<!-- Optional: custom scheme, for example acmeshop://product/123 -->
<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="acmeshop" />
</intent-filter>
</activity>Keep the App Links filter separate from the custom scheme filter, as above. Android verifies App Links per intent filter, and mixing a custom scheme into the verified filter can stop it from verifying.
If you also use a custom domain, add a <data android:scheme="https" android:host="..." /> line (or its own filter) for each domain. Lander serves assetlinks.json on every verified domain of the project.
Keep the activity singleTop
android:launchMode="singleTop" makes links that arrive while the activity is on top go to its onNewIntent instead of starting a second copy of it. With the default standard launch mode, every link would stack a new activity. singleTask also works if your app needs it.
Hand intents to the SDK
The SDK cannot see intents on its own. Pass every intent of that activity to Lander.handleIntent, in both places Android delivers one:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Lander.handleIntent(intent) // cold start: the intent that launched the activity
// ...
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
Lander.handleIntent(intent) // warm start: a link while the app runs
}
}@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Lander.handleIntent(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
Lander.handleIntent(intent);
}Call it for every intent, including the launcher's MAIN intent: the first call tells the SDK whether the app was opened by a link, which is what getInitialLink waits for. Intents without a VIEW action or data are ignored.
handleIntent reads each intent once and marks it (with an extra), so the same link is not delivered again when:
- the activity is recreated after a rotation or another configuration change,
- Android recreates it after killing the app in the background (the SDK sees the saved state and skips the link),
- someone reopens the app from Recents, which replays the original intent with
FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY.
Check verification
Install the app (a build signed by one of the registered keys), then ask Android what it decided:
adb shell pm get-app-links com.acme.shopLook for acme.golander.link: verified. If it says none or legacy_failure, re-run verification after fixing the cause:
adb shell pm verify-app-links --re-verify com.acme.shopOn a test device you can also mark the domain as approved without verification, which is handy while you sort out fingerprints:
adb shell pm set-app-links --package com.acme.shop 2 allInternet permission: nothing to do
The SDK's manifest declares android.permission.INTERNET, and Android merges it into your app. If your app strips merged permissions (tools:node="remove"), keep this one.
Deferred deep links: nothing to do
On first launch the SDK reads the Play Install Referrer once and sends it to Lander. When the install came from a Lander link, the link comes back with confidence DETERMINISTIC and method install_referrer. See Deferred links.
Local development over HTTP
Only needed if you point the SDK at a Lander API running on your machine. The Android emulator reaches your computer at 10.0.2.2, and plain HTTP needs cleartext traffic allowed. Put a network security config in the debug source set so it never ships:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:networkSecurityConfig="@xml/network_security_config" />
</manifest><network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">10.0.2.2</domain>
<domain includeSubdomains="false">localhost</domain>
</domain-config>
</network-security-config>Try it
adb shell am start -a android.intent.action.VIEW \
-d "https://acme.golander.link/p-123" com.acme.shopIf the app is verified it opens on the link's screen. An unverified App Link opens a chooser or the browser instead; see Troubleshooting.