LinksApp
LinksApp developer guide

Build with LinksApp

Use one verified HTTPS address to open an installed app or show the right store destination. Add referral sublinks, public pages, and click insights from the same console.

Verified app links

iOS Universal Links and Android App Links on the app's own host, <app-key>.linksapp.me.

Referral sublinks

Give campaigns or users a code, ref value, expiry, and click limit.

Public pages

Collect links at linksapp.me/<handle> with your chosen theme.

Resolution & analytics

Read the app destination through HTTPS and measure clicks by platform and day.

No Safari scheme error. The browser fallback does not automatically try a custom URL scheme. Installed apps open verified links through the operating system; absent apps see a store page.

Quick start

  1. Create an app link. In the LinksApp console, enter your app's deep destination and store fallbacks.
  2. Register app identity. Add the Apple Team ID and Bundle ID, or Android package name and release SHA-256 signing fingerprint.
  3. Claim the domain in your app. Add Associated Domains on iOS or an auto-verified intent filter on Android.
  4. Handle the URL. Receive the complete HTTPS URL, call /api/resolve, and navigate to effective_deep_link.
Shareable URL
https://your-app.linksapp.me/invite-42

Every app has its own host, so each customer app claims only its own URLs. The bare host https://your-app.linksapp.me is the app link; a sublink adds one path segment. App keys are lowercase letters, numbers and -, because they are host names.

Flutter SDK

The published linksapp Flutter package receives cold-start and later links on both platforms, resolves each URL, and gives your router the destination. Native domain association is still required; Dart code cannot claim a domain.

  1. Register the app in LinksApp. Create an App Link and copy its app key. Save the Android application ID and Play App Signing SHA-256 fingerprint. Save the iOS Team ID, Bundle ID, and numeric App Store ID. These values generate the platform association files served by <app-key>.linksapp.me.
  2. Install the SDK. Add the package to your Flutter app:
    Terminal
    flutter pub add linksapp
  3. Configure Android. Inside the Flutter MainActivity in android/app/src/main/AndroidManifest.xml, add the metadata and filter below. Replace YOUR_APP_KEY; keep the activity's existing launcher filter.
    AndroidManifest.xml
    <meta-data android:name="flutter_deeplinking_enabled"
               android:value="false" />
    
    <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_APP_KEY.linksapp.me" />
    </intent-filter>

    LinksApp publishes /.well-known/assetlinks.json. The package name and signing fingerprint must match the installed release app. Use the Play App Signing certificate, not the upload certificate.

  4. Configure iOS. Add Associated Domains to the Runner target in Xcode, then put the key below inside ios/Runner/Info.plist's <dict>. The Team ID and Bundle ID must match your signed app.
    Xcode and Info.plist
    // Signing & Capabilities → Associated Domains
    applinks:YOUR_APP_KEY.linksapp.me
    
    <key>FlutterDeepLinkingEnabled</key>
    <false/>

    Use applinks:YOUR_APP_KEY.linksapp.me because the shared link URL uses that host; it is not your Bundle ID or your own website domain. LinksApp publishes /.well-known/apple-app-site-association on that host for your app alone. Reinstall the app after changing association settings because iOS caches them.

  5. Start one listener near your router. links.start() receives the launch URL and later URLs, and suppresses the common duplicate initial event. Route result.deepLink yourself and dispose the listener with its owner.
    Flutter
    import 'package:linksapp/linksapp.dart';
    
    final links = LinksApp(
      appKey: 'YOUR_APP_KEY',
      onResolved: (result) {
        router.open(result.deepLink, referral: result.ref);
      },
      onError: (error, stack) {
        reportLinkError(error, stack);
      },
    );
    await links.start();
    // When the owning router is torn down: await links.dispose();
  6. Test both platforms. Tap a real https://YOUR_APP_KEY.linksapp.me link from another app. Android should verify the domain and open your app; iOS should open it when installed. With no app installed, the browser fallback should offer the right store. Typing a URL into Safari's address bar is not a Universal Link test.

Huawei phones (AppGallery). Phones on EMUI or HarmonyOS 2–4 use the Android setup above. Save your AppGallery app ID in LinksApp so visitors without Google Play get an AppGallery button, and if AppGallery re-signs your app, add its signing SHA-256 to the app link's fingerprints. HarmonyOS NEXT is not supported.

Keep credentials on your server. The mobile SDK resolves links; it does not create sublinks. A sublink-creation API key must never be embedded in Flutter, Android, or iOS code.

See the package's Example tab for a complete Flutter widget and the package README for error handling and troubleshooting.

Native iOS SDK

The repo-local packages/linksapp_ios_sdk Swift Package validates your Universal Link and resolves it to a destination. It is not a remote Swift package yet.

  1. Add the SDK. In Xcode choose File → Add Package Dependencies → Add Local, then select packages/linksapp_ios_sdk from a LinksApp source checkout. Import LinksAppSDK in the app target.
  2. Register and claim the domain. Save the exact Apple Team ID, Bundle ID, app key, and numeric App Store ID in LinksApp. Add Associated Domains to the app target:
    Xcode
    applinks:YOUR_APP_KEY.linksapp.me

    Use YOUR_APP_KEY.linksapp.me because it is the host of the link people tap. LinksApp publishes an Apple App Site Association file on that host that claims every path for your app. A customer-owned domain requires its own association file and custom-domain support.

  3. Handle SwiftUI Universal Links. Receive NSUserActivityTypeBrowsingWeb and pass its complete URL to the SDK:
    SwiftUI
    import LinksAppSDK
    
    let linksApp = LinksAppClient(appKey: "YOUR_APP_KEY")
    
    WindowGroup {
      ContentView()
        .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
          guard let url = activity.webpageURL else { return }
          Task {
            do {
              let result = try await linksApp.resolve(url)
              openDestination(result.effectiveDeepLink, ref: result.ref)
            } catch {
              showLinkError(error)
            }
          }
        }
    }
  4. UIKit alternative. If your app uses scenes, call the same resolve(_:) method from scene(_:continue:); apps without scenes can use the application continuation callback. Test by tapping from Notes or Messages, then reinstall after an association change.

First install: the browser fallback provides a Smart App Banner and App Store button. Universal Links alone do not automatically restore a link after an iOS App Store install. The visitor can return to the page and tap Open.

Native Android SDK

The repo-local packages/linksapp_android_sdk/linksapp Gradle module resolves verified App Links off the main thread. It is not published to Maven yet.

  1. Add the SDK. Adjust the checkout path for your project:
    Gradle Kotlin DSL
    // settings.gradle.kts
    include(":linksapp")
    project(":linksapp").projectDir =
        file("../linkcard/packages/linksapp_android_sdk/linksapp")
    
    // app/build.gradle.kts
    dependencies {
        implementation(project(":linksapp"))
    }
  2. Register and claim the domain. Save the Android package name, app key, and Play App Signing SHA-256 in LinksApp. Add this filter inside the activity that receives links:
    AndroidManifest.xml
    <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_APP_KEY.linksapp.me" />
    </intent-filter>
  3. Handle cold and warm Intents. Call the SDK from both lifecycle callbacks. Its result callback runs on the main thread:
    Kotlin
    import me.linksapp.sdk.LinksAppClient
    
    private val linksApp = LinksAppClient(appKey = "YOUR_APP_KEY")
    
    override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      handleLink(intent)
    }
    
    override fun onNewIntent(intent: Intent) {
      super.onNewIntent(intent)
      setIntent(intent)
      handleLink(intent)
    }
    
    private fun handleLink(intent: Intent?) {
      val incoming = intent?.data ?: return
      if (intent.action != Intent.ACTION_VIEW) return
      linksApp.resolve(incoming) { result ->
        result.onSuccess { openDestination(it.effectiveDeepLink, it.ref) }
          .onFailure { showLinkError(it) }
      }
    }
    
    override fun onDestroy() {
      linksApp.close()
      super.onDestroy()
    }
  4. Verify on a device. Confirm Android has associated the domain with your installed package:
    Terminal
    adb shell pm verify-app-links --re-verify YOUR_ANDROID_PACKAGE
    adb shell pm get-app-links YOUR_ANDROID_PACKAGE

For referrals that cross a Play Store install, read the Play Install Referrer separately. Treat a referrer as attribution, not proof of authorization.

REST API

EndpointPurposeAccess
GET /api/resolveResolve an app key and optional sublink code; record the click.Public
POST /api/sublinkCreate a referral code with ref, expiry, or click limit.Per-app API key; server-side only
Resolve
GET https://linksapp.me/api/resolve?app_key=your-app&code=invite-42

200 OK
{
  "app_key": "your-app",
  "code": "invite-42",
  "effective_deep_link": "myapp://invite?ref=user_42",
  "ref": "user_42",
  "fallbacks": { "ios": "...", "android": "...", "web": "..." }
}
Create a sublink
POST https://linksapp.me/api/sublink
Content-Type: application/json

{
  "parent_code": "your-app",
  "api_key": "YOUR_SERVER_SIDE_API_KEY",
  "ref": "user_42",
  "max_clicks": 1
}

GET /api/resolve returns 200, 400, 404, or 410. An API-created sublink responds with its verified https://<app-key>.linksapp.me/<code> URL. Never put the sublink API key in a shipped mobile or web client.

Public link pages

Create a collection in the LinksApp console, choose a handle, add links, and publish. Each page has a shareable https://linksapp.me/<handle> URL, a theme, and link-level analytics. Public pages and app links are separate products: pages collect destinations, while app links route a single tap into an app or store.

Deployment & verification

If you host your own LinksApp deployment, publish Firebase Functions and Hosting together. The Hosting-only GitHub workflow does not deploy Function changes.

Shell
cd functions && npm install
cd .. && flutter build web --release
firebase deploy --only functions,hosting

Check that the association URLs return JSON, not Flutter's index.html. Test installed and absent-app behavior on physical devices. Keep legacy /d/<code> links during migration; iOS legacy pages use explicit buttons rather than probing a missing custom scheme.