Skip to main content

Flutter SDK

Native Flutter SDK for deep linking, mobile attribution, and conversion tracking. Pure Dart implementation with iOS and Android support.

Faster setup with AI

Already using Claude Code, Cursor, or Claude Desktop? The LinkForty MCP server can scaffold your SDK integration automatically. Just ask: "Help me add the LinkForty SDK to my Flutter app."

Features

  • Deferred deep linking -- route new users to specific content after install
  • Direct deep linking -- handle Universal Links, App Links, and custom URL schemes
  • Install attribution -- probabilistic fingerprint matching
  • Event tracking -- log in-app events tied to attribution data
  • Revenue tracking -- structured purchase tracking with dedicated method
  • Offline event queue -- events persist across app restarts and retry automatically
  • Programmatic link creation -- create short links from your app
  • Pure Dart -- 100% Dart, standard Future/Stream APIs
  • Privacy-first -- no IDFA/GAID collection

Requirements

  • Flutter 3.10+
  • Dart 3.0+
  • iOS 12.0+
  • Android API 21+

Installation

Add to your pubspec.yaml:

dependencies:
linkforty_flutter: ^0.2.0

Or run:

flutter pub add linkforty_flutter

Quick Start

Initialize the SDK

Initialize once at app startup, typically in main.dart:

import 'package:linkforty_flutter/link_forty.dart';
import 'package:linkforty_flutter/models/link_forty_config.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();

try {
final config = LinkFortyConfig(
baseURL: Uri.parse('https://go.yourdomain.com'),
apiKey: 'your-api-key', // optional -- required for link creation
debug: true,
attributionWindowHours: 168, // 7 days (default)
);

final response = await LinkForty.initialize(config: config);
print('LinkForty initialized. Install ID: ${response.installId}');
} catch (e) {
print('LinkForty initialization failed: $e');
}

runApp(const MyApp());
}
LinkForty.instance.onDeferredDeepLink((deepLinkData) {
if (deepLinkData != null) {
print('Attributed install: ${deepLinkData.shortCode}');
final productId = deepLinkData.customParameters?['productId'];
if (productId != null) {
navigatorKey.currentState?.pushNamed('/product', arguments: productId);
}
} else {
print('Organic install');
}
});

The SDK uses app_links internally to handle incoming links. Register a callback:

LinkForty.instance.onDeepLink((uri, deepLinkData) {
print('Deep link opened: $uri');
if (deepLinkData != null) {
if (deepLinkData.deepLinkPath != null) {
navigatorKey.currentState?.pushNamed(deepLinkData.deepLinkPath!);
}
}
});
Server-side resolution

Deep links are automatically resolved via the server for enriched data including deepLinkPath, appScheme, and linkId. If the server is unreachable, the SDK falls back to local URL parsing.

Track Events

// Custom event
await LinkForty.instance.trackEvent('add_to_cart', {
'product_id': '123',
'category': 'electronics',
});

// Revenue event
await LinkForty.instance.trackRevenue(
amount: 29.99,
currency: 'USD',
properties: {'product_id': '123'},
);

Every event is automatically credited to the deep link that most recently opened the app (last-click attribution), along with an app-open session — so analytics can show what users do after clicking a link. Events with no preceding deep-link open are reported as organic. No extra code is required.

Track Screen Views

Reporting screen views lets analytics build a per-link screen-flow funnel. Each screen_view carries the same last-click attribution as your other events.

Automatic — add LinkFortyNavigatorObserver to your app's navigatorObservers; named routes are reported as they appear:

import 'package:linkforty_flutter/linkforty_flutter.dart';

MaterialApp(
navigatorObservers: [LinkFortyNavigatorObserver()],
// ...
);

Manual — call it yourself (e.g. for screens not driven by named routes):

await LinkForty.instance.trackScreenView('ProductDetail');
import 'package:linkforty_flutter/models/create_link_options.dart';
import 'package:linkforty_flutter/models/utm_parameters.dart';

final result = await LinkForty.instance.createLink(
CreateLinkOptions(
templateId: 'your-template-id',
deepLinkParameters: {'route': 'PRODUCT', 'id': '123'},
title: 'Check out this product',
utmParameters: UTMParameters(source: 'app', campaign: 'share'),
),
);

print(result.url); // https://go.yourdomain.com/tmpl/abc123
print(result.shortCode); // abc123
print(result.linkId); // uuid
warning

Link creation requires an API key and a template ID. The link creation endpoint is only available on LinkForty Cloud, not self-hosted Core.


Platform Setup

Android

Add intent filters to your AndroidManifest.xml:

<activity ...>
<!-- App Links (HTTPS) -->
<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="go.yourdomain.com" />
</intent-filter>

<!-- Custom Scheme (Optional) -->
<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="yourapp" />
</intent-filter>
</activity>

Your LinkForty server automatically hosts the Digital Asset Links file at /.well-known/assetlinks.json.

iOS

  1. In Xcode, enable the Associated Domains capability
  2. Add: applinks:go.yourdomain.com
  3. Optionally add your custom URL scheme under URL Types in Info.plist

Your LinkForty server automatically hosts the Apple App Site Association file at /.well-known/apple-app-site-association.


API Reference

Initialization

LinkForty.initialize(config:)

Initialize the SDK. Must be called before any other method. Returns an InstallResponse with attribution results.

static Future<InstallResponse> initialize({
required LinkFortyConfig config,
int attributionWindowHours = 168,
String? deviceId,
})
ParameterTypeRequiredDefaultDescription
configLinkFortyConfigYes--SDK configuration
attributionWindowHoursintNo168Attribution window in hours (1--2160)
deviceIdString?NonullOptional device ID for high-confidence matching

LinkFortyConfig parameters:

ParameterTypeRequiredDefaultDescription
baseURLUriYes--Your LinkForty server URL
apiKeyString?NonullAPI key for link creation and Cloud features
debugboolNofalseEnable verbose logging
attributionWindowHoursintNo168Attribution window in hours (1--2160)

LinkForty.instance

Returns the shared SDK instance. Throws NotInitializedError if not initialized.

LinkForty.instanceOrNull

Returns the shared SDK instance, or null if not initialized.

Deep Linking

onDeferredDeepLink(callback)

Register a callback for deferred deep links. If data is already available, the callback fires immediately.

void onDeferredDeepLink(DeferredDeepLinkCallback callback)

onDeepLink(callback)

Register a callback for direct deep links. The SDK resolves Universal Links / App Links server-side for enriched data.

void onDeepLink(DeepLinkCallback callback)

handleDeepLink(uri)

Manually pass a URI for the SDK to process.

Future<void> handleDeepLink(Uri uri)

Event Tracking

trackEvent(name, properties?)

Track a custom in-app event.

Future<void> trackEvent(String name, [Map<String, dynamic>? properties])

trackRevenue(amount:, currency:, properties:)

Track a revenue event with structured amount and currency.

Future<void> trackRevenue({
required double amount,
required String currency,
Map<String, dynamic>? properties,
})

trackScreenView(name, properties?)

Report a screen view for screen-flow funnels. For automatic tracking, add LinkFortyNavigatorObserver to your app's navigatorObservers instead.

Future<void> trackScreenView(String name, [Map<String, dynamic>? properties])

Event Queue

flushEvents()

Manually send all queued events.

Future<void> flushEvents()

clearEventQueue()

Remove queued events without sending them.

void clearEventQueue()

queuedEventCount

Getter returning the number of events waiting to be sent.

int get queuedEventCount

createLink(options)

Create a short link programmatically. Requires an API key and template ID.

Future<CreateLinkResult> createLink(CreateLinkOptions options)
OptionTypeRequiredDescription
templateIdString?NoTemplate UUID (uses organization's default template if omitted)
templateSlugString?NoTemplate slug for URL construction
deepLinkParametersMap<String, String>?NoIn-app routing parameters
titleString?NoLink title
descriptionString?NoLink description
customCodeString?NoCustom short code
utmParametersUTMParameters?NoCampaign tracking parameters
externalUserIdString?NoUser ID for deduplication and share attribution

Data Access

getInstallId()

String? getInstallId()

getInstallData()

DeepLinkData? getInstallData()

isFirstLaunch()

bool isFirstLaunch()

Data Management

clearData()

Wipe all locally stored SDK data.

Future<void> clearData()

reset()

Return the SDK to an uninitialized state so it can be re-initialized.

void reset()

Error Handling

All errors extend the LinkFortyError base class:

import 'package:linkforty_flutter/errors/link_forty_error.dart';

try {
await LinkForty.instance.createLink(CreateLinkOptions(title: 'Test'));
} on MissingApiKeyError {
print('API key required for link creation');
} on MissingTemplateIdError {
print('Template ID required for link creation');
} on NotInitializedError {
print('Call initialize() first');
} on NetworkError catch (e) {
print('Network unavailable: $e');
} on LinkFortyError catch (e) {
print('SDK error: $e');
}

Error Types

ErrorTrigger
NotInitializedErrorMethod called before initialize()
AlreadyInitializedErrorDuplicate initialize() call
InvalidConfigurationErrorInvalid config (e.g., non-HTTPS URL, out-of-range attribution window)
NetworkErrorFailed network request
InvalidResponseErrorNon-2xx server response
DecodingErrorFailed JSON parsing
EncodingErrorFailed request encoding
InvalidEventDataErrorInvalid event data
InvalidDeepLinkUrlErrorMalformed deep link URL
MissingApiKeyErrorcreateLink() called without an API key
MissingTemplateIdErrorcreateLink() called without a template ID

Offline Resilience

Events are automatically queued when the network is unavailable:

  • Queue persists across app restarts
  • Events are retried automatically when connectivity is restored
  • Use flushEvents() to manually trigger a send
  • Use clearEventQueue() to discard queued events without sending
  • Check queuedEventCount to monitor queue depth

Privacy

The Flutter SDK is designed with privacy in mind:

  • No IDFA/GAID collection -- attribution uses probabilistic fingerprinting only
  • Minimal data collection -- timezone, language, screen resolution, OS version, app version
  • User control -- clearData() for GDPR/CCPA compliance
  • HTTPS enforced -- all communication uses HTTPS (except localhost and 10.0.2.2 for development)

Configuration Validation

The SDK validates configuration at initialization:

  • Base URL must use HTTPS (except localhost, 127.0.0.1, 0.0.0.0, and 10.0.2.2 for local development)
  • Attribution window must be between 1 and 2160 hours (90 days)
  • Invalid configuration throws InvalidConfigurationError

Self-Hosted Configuration

For self-hosted LinkForty Core, omit the API key:

final config = LinkFortyConfig(
baseURL: Uri.parse('https://links.yourcompany.com'),
debug: false,
);
await LinkForty.initialize(config: config);

Troubleshooting

iOS:

  1. Verify Associated Domains capability is enabled with the correct domain
  2. Check that the AASA file is accessible at https://go.yourdomain.com/.well-known/apple-app-site-association
  3. Apple caches AASA files; changes can take time to propagate

Android:

  1. Verify android:autoVerify="true" in your intent filter
  2. Check that assetlinks.json is accessible at https://go.yourdomain.com/.well-known/assetlinks.json
  3. Verify SHA-256 fingerprint matches your keystore
  1. Check debug logs (debug: true) for install reporting errors
  2. Ensure the attribution window hasn't expired
  3. Fingerprint matching depends on network conditions -- VPNs and privacy relays reduce accuracy

AlreadyInitializedError

The SDK guards against double initialization. If you need to reinitialize, call reset() first:

LinkForty.instance.reset();
await LinkForty.initialize(config: newConfig);

Events not appearing in analytics

  1. Check queuedEventCount -- events may be queued due to network issues
  2. Call flushEvents() to manually send queued events
  3. Ensure the SDK has a valid install ID (check with getInstallId())

Next Steps

Resources