Flutter SDK
Native Flutter SDK for deep linking, mobile attribution, and conversion tracking. Pure Dart implementation with iOS and Android support.
Using Claude Code, Cursor or Codex? Copy this prompt and paste it into your agent from the root of your project. It installs the SDK, wires deep links through your navigator (asking you which screen each link should open), adds one event, and verifies the install in your LinkForty dashboard.
Show the prompt
I use LinkForty (https://linkforty.com), a deep-linking and mobile attribution platform: short links that route each click to the app, the app store or the web, and record clicks, installs and in-app events per link.
Before writing any code, read the LinkForty documentation index at https://docs.linkforty.com/llms-full.txt and the pages listed under "Docs" below. Work from what the docs say, not from assumptions about how similar products work. If something you need is not in the docs or in this prompt — an ID, a token, a URL — stop and ask me for it rather than inventing a placeholder that looks real.
## Task: install the LinkForty Flutter SDK
Add the LinkForty SDK to this app so that an install that follows a click on one of my links is attributed to that link, a link's deep-link path opens the right screen on first launch (deferred deep linking) and on later opens (Universal Links / App Links), and one key in-app event is recorded.
## Values
- **App token**: shown in the LinkForty dashboard under Workspace Settings → App Token (it starts with `at_`). Ask me for it; put it in the app's config or environment, not hard-coded in a committed source file.
- **Base URL**: `https://api.linkforty.com` for LinkForty Cloud. (Only a self-hosted LinkForty server uses a different origin — ask if you are not sure which this is.)
- **Link host**: the host my links are on, shown in the dashboard under Settings → Domains — the workspace's own host (it looks like `something.toapp.me`) and any verified custom domain listed there. Ask me for it. This is the domain the app must claim for Universal Links / App Links.
- **LinkForty MCP server** (optional): if it is configured in this environment (https://mcp.linkforty.com/mcp, or the `linkforty-mcp` command), use `get_app_config` to read the workspace's app settings and `create_link` to make a test link at the end. Do not set it up for me if it is not.
## How to work
1. **Look before you change anything.** Identify the framework, its version, the navigation/router library and where the app initializes at launch. List the screens (routes) you found, and **ask me which screen each incoming link should open and what parameter it needs** — for example "a link with `productId` opens the product screen". Wait for my answer before editing; do not guess routes.
2. **Work on a new git branch.** If the working tree has uncommitted changes, stop and tell me instead of building on top of them.
3. **Follow the docs pages named below, not memory.** Package names, method names and config keys come from those pages; LinkForty's SDKs are not the same as other attribution SDKs you may know.
4. **Do not invent values.** Anything in "Values" that says "ask me" is something you ask for. Never commit a token to source.
5. **Keep the app working without a link.** A launch with no matching click must behave exactly as before: no crash, no error surfaced to the user, no blocked start-up.
6. **Finish with a summary**: every file you changed and why; the manual steps that remain for me (Apple Team ID and bundle ID, Play signing SHA-256 fingerprint, store listing, a rebuild and reinstall) and where in the LinkForty dashboard each one goes (Workspace Settings → App Configuration); and anything you were unsure about.
7. **Verify.** Run the app once, then check the LinkForty dashboard: the SDK Doctor badge in the sidebar (https://docs.linkforty.com/guides/attribution/sdk-health) should report the SDK as installed. If the MCP server is available, create a test link with `create_link` and open it on a device or simulator; the click and the deep link should reach the screen we agreed on.
## Steps
1. **Install** with `flutter pub add linkforty_flutter` (https://docs.linkforty.com/sdks/flutter#installation).
2. **Initialize once** in `main()` after `WidgetsFlutterBinding.ensureInitialized()`: build a `LinkFortyConfig(baseURL: Uri.parse(…), appToken: …, debug: kDebugMode)` with the base URL and app token from "Values" and `await LinkForty.initialize(config: config)` inside a try/catch — a failure is logged, never fatal, and `runApp` still runs (https://docs.linkforty.com/sdks/flutter#initialize-the-sdk). Leave `apiKey` out — it is only for creating links from the app.
3. **Claim the link host** (https://docs.linkforty.com/sdks/flutter#platform-setup): Android — an `<intent-filter android:autoVerify="true">` for `https://<link host>` on the main activity in `AndroidManifest.xml`; iOS — the Associated Domains capability with `applinks:<link host>` in the Runner target. One entry per host if there is also a custom domain. LinkForty serves the AASA and assetlinks.json files on that host itself; nothing to upload. Tell me the Apple Team ID / bundle ID and Android package / signing SHA-256 must be entered in LinkForty under Workspace Settings → App Configuration, or the OS will not verify the domain.
4. **Deferred deep link**: register `LinkForty.instance.onDeferredDeepLink((data) { … })` right after initialize. When `data` is non-null, route to the screen we agreed on using its `deepLinkPath` / `customParameters` through the app's existing navigator (a `navigatorKey`, go_router, or whatever it uses); when it is null (organic install) do nothing (https://docs.linkforty.com/sdks/flutter#handle-deferred-deep-links).
5. **Direct deep links**: register `LinkForty.instance.onDeepLink((uri, data) { … })` and route the same way (https://docs.linkforty.com/sdks/flutter#handle-direct-deep-links). The SDK handles the incoming-link plumbing itself; do not add a second app_links / uni_links listener for the link host.
6. **One in-app event**: at the obvious success moment in this app (sign-up completed, purchase, or the first key action — tell me which you chose) call `trackEvent` with `signup`, or `trackRevenue` with amount and ISO currency for a purchase (https://docs.linkforty.com/sdks/flutter#track-events). No personal data in properties.
## Docs
- Flutter SDK: https://docs.linkforty.com/sdks/flutter (Installation, Initialize the SDK, Handle Deferred Deep Links, Handle Direct Deep Links, Platform Setup, Track Events)
- SDK integration overview: https://docs.linkforty.com/guides/attribution/sdk-integration
- Deferred deep linking: https://docs.linkforty.com/guides/attribution/deferred-deep-linking
- Link hosts and app association files: https://docs.linkforty.com/guides/links/custom-domains#your-linkforty-domain
- SDK Doctor: https://docs.linkforty.com/guides/attribution/sdk-health
## Done when
- Opening one of my links on a device without the app, installing it and launching it lands on the screen we agreed on, and the install appears under Installs in the LinkForty dashboard attributed to that link.
- Opening the same link with the app installed goes straight to that screen without a browser stop.
- The in-app event shows under Events, attached to that install.
- SDK Doctor in the dashboard sidebar reports the SDK as installed.
When you are done, tell me: exactly which files changed and why, how I can verify it worked end to end (what to tap or run, and what I should see in the LinkForty dashboard), and anything you were unsure about.It needs your app token; the prompt asks you for it, and the MCP server can supply it if you have that set up.
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/StreamAPIs - 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
appToken: 'at_your_app_token', // recommended for Cloud -- enables organic-install attribution
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());
}
Handle Deferred Deep Links
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');
}
});
Handle Direct Deep Links
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!);
}
}
});
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');
Create Links
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
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="acme.toapp.me" />
</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>
Use your LinkForty domain as the host, and add a second <data> element for each custom domain. Your LinkForty server automatically hosts the Digital Asset Links file at /.well-known/assetlinks.json on every one of those hosts.
iOS
- In Xcode, enable the Associated Domains capability
- Add your link host:
applinks:acme.toapp.mefor your LinkForty domain, andapplinks:links.example.comfor each custom domain - 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 on every one of those hosts.
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,
})
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
config | LinkFortyConfig | Yes | -- | SDK configuration |
attributionWindowHours | int | No | 168 | Attribution window in hours (1--2160) |
deviceId | String? | No | null | Optional device ID for high-confidence matching |
LinkFortyConfig parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
baseURL | Uri | Yes | -- | Your LinkForty server URL |
apiKey | String? | No | null | API key for link creation and Cloud features |
debug | bool | No | false | Enable verbose logging |
attributionWindowHours | int | No | 168 | Attribution 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
Link Creation
createLink(options)
Create a short link programmatically. Requires an API key and template ID.
Future<CreateLinkResult> createLink(CreateLinkOptions options)
| Option | Type | Required | Description |
|---|---|---|---|
templateId | String? | No | Template UUID (uses organization's default template if omitted) |
templateSlug | String? | No | Template slug for URL construction |
deepLinkParameters | Map<String, String>? | No | In-app routing parameters |
title | String? | No | Link title |
description | String? | No | Link description |
customCode | String? | No | Custom short code, up to 20 characters; must not already be used in your workspace (409 SHORT_CODE_TAKEN) |
utmParameters | UTMParameters? | No | Campaign tracking parameters |
externalUserId | String? | No | User 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
| Error | Trigger |
|---|---|
NotInitializedError | Method called before initialize() |
AlreadyInitializedError | Duplicate initialize() call |
InvalidConfigurationError | Invalid config (e.g., non-HTTPS URL, out-of-range attribution window) |
NetworkError | Failed network request |
InvalidResponseError | Non-2xx server response |
DecodingError | Failed JSON parsing |
EncodingError | Failed request encoding |
InvalidEventDataError | Invalid event data |
InvalidDeepLinkUrlError | Malformed deep link URL |
MissingApiKeyError | createLink() called without an API key |
MissingTemplateIdError | createLink() 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
queuedEventCountto 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.2for development)
Configuration Validation
The SDK validates configuration at initialization:
- Base URL must use HTTPS (except
localhost,127.0.0.1,0.0.0.0, and10.0.2.2for 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
Universal Links / App Links not opening the app
iOS:
- Verify Associated Domains capability is enabled with the correct domain
- Check that the AASA file is accessible at
https://go.yourdomain.com/.well-known/apple-app-site-association - Apple caches AASA files; changes can take time to propagate
Android:
- Verify
android:autoVerify="true"in your intent filter - Check that
assetlinks.jsonis accessible athttps://go.yourdomain.com/.well-known/assetlinks.json - Verify SHA-256 fingerprint matches your keystore
Deferred deep link not firing
- Check debug logs (
debug: true) for install reporting errors - Ensure the attribution window hasn't expired
- 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
- Check
queuedEventCount-- events may be queued due to network issues - Call
flushEvents()to manually send queued events - Ensure the SDK has a valid install ID (check with
getInstallId())
Next Steps
- Create links in the dashboard
- View analytics for your app
- Configure attribution windows
- SDK Specification -- full feature reference