Flutter SDK
Install and integrate the LinkTrail Flutter SDK — package linktrail_flutter, one Dart API over the native iOS and Android SDKs.
A thin plugin over the native LinkTrail SDKs · package linktrail_flutter 0.1.0 on pub.dev · entry point LinkTrail · Android minSdk 26, iOS 15+. Wraps io.linktrail:sdk 0.1.0 (Maven Central) and LinkTrailSDK 0.1.0 (CocoaPods) behind one Dart API.
Before this works, set up three things in the dashboard. Skip one and links fail silently — they open the browser instead of your app. Full flow: Quickstart.
- 1Register both app targets — Apps → Register app. iOS needs your Apple Team ID and bundle ID; Android needs its package name and SHA-256 fingerprints.
- 2Register the identifier your build really has — the SDK sends the iOS bundle ID and the Android
applicationId, and it must equal the identifier you registered, in the same workspace as the API key. A debug build with a suffixed ID (com.acme.shop.debug) needs its own registered app, or every install answers 404 (onErrorreports it as a server error). - 3Add a link domain — Domains. Or use your free
*.linktrail.iosubdomain. - 4Create an API key — Settings → API Keys. That is the
lt_live_…value below.
Install
Shellflutter pub add linktrail_flutterOr add it to your app's
pubspec.yaml, thenflutter pub get:pubspec.yamlYAMLdependencies: linktrail_flutter: ^0.1.0The native SDKs are pulled in automatically — no manual Gradle or CocoaPods edits. Platform minimums the SDK requires (set them if your app targets lower):
android { defaultConfig { minSdk = 26 } }andplatform :ios, '15.0'.Integrate
main.dartDartimport 'package:flutter/material.dart'; import 'package:linktrail_flutter/linktrail_flutter.dart'; // MyApp and router are your own. Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); // One stream — fires for deferred (first-launch) AND re-engagement links. // Subscribe BEFORE configure so a cold-start link is not missed: LinkTrail.onLink.listen((event) { router.route(event.link.path, event.link.customData); }); LinkTrail.onError.listen((error) => debugPrint('LinkTrail: $error')); // The API key is required. The install is sent automatically — once your app // records a consent decision (see Consent below). await LinkTrail.configure(apiKey: 'lt_live_...'); runApp(const MyApp()); }customDatais a map of strings to strings. A nested object or array set on the link arrives as a compact JSON string — decode it yourself.Incoming links are captured automatically — you do not need to override
MainActivity(Android) orAppDelegate/SceneDelegate(iOS). The plugin forwards App Links, Universal Links, and custom-scheme opens to the SDK on both cold start and while running. Every callback is a broadcast Stream, so you can listen from multiple places.Consent
Consent gating is on by default (
requireConsent: true) and it holds the install: nothing is sent and the deferred link does not route until your app records a decision.setConsent(true)sends the install counted and attributed and routes the deferred link;setConsent(false)sends one uncounted install that still resolves it, so the user lands in the right place. Re-engagement links (app already installed) route regardless. There is no consent getter — persist the choice yourself, and on every launch subscribe toonLinkand replay the stored decision withsetConsentright afterconfigure. Full model, CMP integration and store questionnaires: Consent & privacy.ImportantAn app that never calls
setConsentgets no deferred link and no attribution. Wire both answers of your consent prompt tosetConsent, or passrequireConsent: falseif consent is collected elsewhere.Dart// Call once your consent UI resolves — and replay the stored decision on every // launch, right after configure: await LinkTrail.setConsent(true); // grant → install sent + attributed, deferred link routes, queue flushed await LinkTrail.setConsent(false); // deny → one uncounted install, deferred link still routes, queue cleared // Collecting consent elsewhere (a CMP)? Opt out of the gate — tracking runs from configure, // and an explicit setConsent(false) still wins: await LinkTrail.configure(apiKey: 'lt_live_...', options: LinkTrailOptions(requireConsent: false));Deep-link setup
The plugin captures links automatically, but the OS still needs to route the link to your app. Android: declare your App Links host in
AndroidManifest.xml(see below). iOS: add the Associated Domains capability withapplinks:yourapp.linktrail.io.WarninghandleDeepLinkclaims onlyhttpslinks onlinktrail.io(or a subdomain) and on the hosts listed inlinkDomains(or their subdomains). Custom domains must be listed: links ongo.yourbrand.comneedLinkTrailOptions(linkDomains: ['go.yourbrand.com']), or the app opens and never navigates. Any other URL (mailto:, an OAuth callback,myapp://settings) returnsfalsefromhandleDeepLinkwith no request and noonError, so route it yourself. Custom-scheme LinkTrail links (myapp://slug) are no longer handled; use thehttpslink. The full rules: Which links the SDK claims. Upgrading from an earlier version? See Upgrading to 0.1.0.android/app/src/main/AndroidManifest.xmlXML<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="yourapp.linktrail.io" /> </intent-filter>WarningThe
apple-app-site-association/assetlinks.jsonfiles are generated and hosted by LinkTrail from the credentials you registered in the dashboard. On iOS the plugin registers on the UIScene lifecycle, so links are delivered on both the scene and classic app-delegate lifecycles.Verify it works
Every failure on this page is silent unless you look in the right place. Check these in order:
- 1SDK logs. Pass
LinkTrailOptions(logEnabled: true, logLevel: LinkTrailLogLevel.debug)toconfigureand read the native logs — the Xcode console (lines prefixed[LinkTrail]) oradb logcat -s LinkTrail. On the first launch expectInstall held — awaiting consent decision; aftersetConsent(true)expectConsent granted,Sending install event… (consent: true), thenInstall attributed—Install attributed (deterministic click token)when the click token was recovered — orOrganic install (no attribution match). Both platforms print[LinkTrail] [LEVEL] message, so these lines read[LinkTrail] [INFO] …on iOS and Android (Android logs them under the LinkTrail tag). No lines at all meansconfigurenever ran. - 2API key Last used. Dashboard → Settings → API Keys: the key's Last used changes from Never after the first call made with a valid key.
- 3Overview and Links. Dashboard → Overview: the installs count goes up. Links → your link shows a click and an install.
- 4onError. A wrong or revoked key does not fail
configure. The backend answers 401 with{"code":401,"message":"Invalid or missing API key"}and the SDK reports it throughonErrorasLinkTrailInvalidApiKeyException— but only once the install is sent: aftersetConsent, or atconfigurewithrequireConsent: false. Nothing is sent before that. - 5Re-test deferred without reinstalling. Call
LinkTrail.resetForTesting()— it clears the install flag, cached attribution, device ID and the consent decision, so the next launch behaves like a genuine first launch and asks for consent again. Testing only; never ship it.
[LinkTrail] [INFO] Install held — awaiting consent decision [LinkTrail] [INFO] Consent granted [LinkTrail] [INFO] Sending install event… (consent: true) [LinkTrail] [INFO] Install attributed (deterministic click token) [LinkTrail] [INFO] Install attributed [LinkTrail] [INFO] Organic install (no attribution match)Still nothing? Troubleshooting lists the usual causes by symptom.
- 1SDK logs. Pass
More
// Custom + revenue events:
await LinkTrail.trackEvent(name: 'purchase', value: 59.99, currency: 'USD');
// Cached results:
final attribution = await LinkTrail.lastAttribution;
final lastLink = await LinkTrail.lastDeepLink;
// Attribution stream (fires when an install is attributed):
LinkTrail.onAttribution.listen((attribution) { /* … */ });
// Manual install (rare): autoTrackInstall: false, then call trackInstall() yourself.
// Still subject to the consent gate above — call it after setConsent.
await LinkTrail.configure(apiKey: 'lt_live_...',
options: LinkTrailOptions(autoTrackInstall: false));
await LinkTrail.trackInstall();
// iOS ATT / SKAdNetwork (on Android, requestTrackingAuthorization returns true; the rest are no-ops):
await LinkTrail.requestTrackingAuthorization();
await LinkTrail.registerForSKAdAttribution();
await LinkTrail.updateConversionValue(3, coarseValue: LinkTrailCoarseConversionValue.medium);Errors arrive as typed LinkTrailException subtypes on onError, and are thrown from the Future-returning calls — so you can try/catch a specific case like LinkTrailInvalidApiKeyException.
Deferred attribution & the paste button (iOS)
On iOS, deferred attribution recovers a click token the tapped link leaves on the clipboard, and LinkTrailOptions.clickTokenSource chooses how it is read. The widget renders the native control on iOS 16+ and nothing on Android, where the Play Install Referrer handles deferred attribution instead.
| clickTokenSource | Behaviour | Trade-off |
|---|---|---|
| pasteButton (default) | Read only when the user taps a LinkTrailPasteButton. | No system alert, but it needs the widget and autoTrackInstall: false. |
| automatic | The SDK reads the clipboard itself at install. | No UI, but iOS shows the “Allow Paste” alert on first launch. |
| none | The clipboard is never read, so no click token is recovered. | No alert and no UI; deferred matching falls back to probabilistic only. |
With the default pasteButton you must render the widget and set autoTrackInstall: false, or deferred installs on iOS stay unattributed. Apple restricts customization of the control — no custom label text, font, or border.
// The widget fills its parent's width — wrap it in a SizedBox to constrain it.
LinkTrailPasteButton(
height: 48,
// null (default): the tap grants consent only while it is still undecided.
// true: always grants, even after a denial. false: never touches consent.
grantsConsent: null,
onToken: (token) {
// The SDK has already sent the install with the token; advance your UI here.
},
)The widget uses the native paste handling: the tap records consent per grantsConsent and sends the install with the click token itself, so onToken is a side-effect hook — don't call trackInstallWithClickToken from it. Using a CMP that answers later? Pass grantsConsent: false: the install is held with the token until your own setConsent. Its parameters are grantsConsent, onToken, color and height; there is no width parameter.
API surface
| API | Purpose |
|---|---|
| configure(apiKey:, options:) | Initialize; the install is held until a setConsent decision |
| setConsent(granted) | Record the decision; releases the held install either way (see Consent above) |
| onLink — Stream<LinkTrailLinkEvent> | Deep link delivered (deferred + re-engagement) |
| onAttribution — Stream<LinkTrailAttribution> | Install attribution result |
| onError — Stream<LinkTrailException> | Failures (invalid key, network…) |
| handleDeepLink(uri) | Resolve a link manually (links are also auto-captured) |
| trackEvent(name:, value:, currency:) | Custom + revenue events |
| trackInstall(force:) | Manual install track (with autoTrackInstall: false) |
| trackInstallWithClickToken(token, force:) | Install carrying a pasted click token (iOS; same as trackInstall on Android) |
| lastAttribution / lastDeepLink | Cached last results |
| requestTrackingAuthorization() | ATT prompt (iOS; returns true on Android) |
| registerForSKAdAttribution() / updateConversionValue(…) | SKAdNetwork (iOS; no-op on Android) |
| resetForTesting() | Clears the install flag, attribution, device ID and consent — testing only |
Example app: KickFlip — a Flutter storefront in the linktrail-flutter repo's example folder, wired end to end. cd example && flutter run --dart-define=LINKTRAIL_API_KEY=lt_live_…. Source · example app · issue tracker · changelog.
Step-by-step walkthrough for both platforms: Deferred deep linking in Flutter.