Add the dependency to your pubspec.yaml:
dependencies:
dynalink_flutter: ^0.0.18
flutter pub get
Add both intent filters to AndroidManifest.xml:
<!-- Custom scheme (fallback) -->
<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="dynalink-{projectId}" android:host="dynalink.app" />
</intent-filter>
<!-- App Links (verified) -->
<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="{projectPrefix}.dynalink.app" />
</intent-filter>
Add the Asset Links JSON in the Admin Panel under Project → Settings. DynaLink
serves it at https://{projectPrefix}.dynalink.app/.well-known/assetlinks.json.
<meta-data android:name="flutter_deeplinking_enabled" android:value="true" />, your router will also try to navigate to /{code} when a link opens the app. Remove it or set it to false so DynaLink owns the link handling.
Serving links from your own domain? Add it in the Admin Panel, list it in the
intent-filter above as well, and pass it to initialize — see
Domains.
Add to ios/Runner/Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>{bundleId}</string>
<key>CFBundleURLSchemes</key>
<array>
<string>dynalink-{projectId}</string>
</array>
</dict>
</array>
<key>NSUserActivityTypes</key>
<array>
<string>NSUserActivityTypeBrowsingWeb</string>
</array>
<key>CFBundleAssociatedDomains</key>
<array>
<string>applinks:{projectPrefix}.dynalink.app</string>
</array>
<key>FlutterDeepLinkingEnabled</key>
<false/>
Add the Apple App Site Association file in the Admin Panel under Project → Settings.
Call initialize before runApp:
import 'package:dynalink_flutter/dynalink_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Dynalink.initialize(
publicKey: 'YOUR_PROJECT_KEY',
projectId: 'YOUR_PROJECT_ID',
// Optional — hosts other than {projectPrefix}.dynalink.app that serve your
// links. Declaring them makes link handling work on the very first link and
// without a network round-trip; otherwise the SDK resolves them from
// GET /api/projects/link-domains the first time it sees an unknown host.
customDomains: const ['links.yourbrand.com'],
);
runApp(const MyApp());
}
Call it before runApp so no link is missed: the stream replays its last event,
so a listener attached later still receives it.
Listen to dynamicLinkStream to receive every processed link:
Dynalink.instance.dynamicLinkStream.listen((DynalinkEvent event) {
// Navigate to the resolved destination
navigateTo(event.actualUrl);
// Campaign / UTM context
if (event.hasCampaign) {
analytics.setCampaign(event.campaignId, event.utmCampaign);
}
// Forward click IDs to your ad-network measurement SDKs
if (event.gclid != null) googleAds.reportConversion(event.gclid!);
if (event.fbclid != null) facebookCapi.reportInstall(event.fbclid!, event.fbclidCapturedAt);
if (event.ttclid != null) tiktokEvents.reportInstall(event.ttclid!);
});
A DynaLink reaches your app through one of two paths, and the SDK handles both — but they behave differently, which matters when you debug.
Through the loading page. The browser opens https://{projectPrefix}.dynalink.app/{code},
the page fingerprints the device, stores the click IDs, then bounces into the app
through the dynalink-{projectId}:// scheme with the destination and the campaign
context already in the URL. No API call is needed.
Directly, by the OS. Once assetlinks.json (Android) or the Apple App Site
Association file (iOS) is verified, tapping the same link opens your app
without loading the page at all. The app then receives the short URL, so the
SDK resolves the destination through GET /api/links/{code} and reports the click
through POST /api/links/{code}/click — which is what keeps click counts, unique
visitors and geo data accurate on this path.
Two rules follow:
event.actualUrl. Never parse the incoming URL path
yourself — on the direct path it only contains the short code.adb shell pm get-app-links {yourPackage}. A domain in the verified state means links now open the app directly.
| Field | Type | Description |
|---|---|---|
actualUrl |
String |
Resolved destination URL |
campaignId |
int? |
Campaign ID |
campaignName |
String? |
Campaign display name |
utmSource |
String? |
UTM source |
utmMedium |
String? |
UTM medium |
utmCampaign |
String? |
UTM campaign slug |
utmTerm |
String? |
UTM term |
utmContent |
String? |
UTM content |
gclid |
String? |
Google Ads click ID |
gbraid |
String? |
Google Ads enhanced click ID (iOS 14.5+) |
fbclid |
String? |
Meta (Facebook/Instagram) click ID |
fbclidCapturedAt |
DateTime? |
When fbclid was first captured in the browser |
ttclid |
String? |
TikTok click ID |
twclid |
String? |
X (Twitter) click ID |
liFatId |
String? |
LinkedIn first-party ad tracking ID |
attributedAt |
DateTime? |
When the backend confirmed the install match |
hasCampaign |
bool |
Convenience getter — true when campaignId != null |
hasClickIds |
bool |
Convenience getter — true when any click ID is non-null |
Click IDs are written to SharedPreferences automatically. Use these getters if you need the values outside of the stream:
final gclid = await Dynalink.instance.getLastGclid();
final gbraid = await Dynalink.instance.getLastGbraid();
final fbclid = await Dynalink.instance.getLastFbclid();
final ttclid = await Dynalink.instance.getLastTtclid();
final twclid = await Dynalink.instance.getLastTwclid();
final liFatId = await Dynalink.instance.getLastLiFatId();
final campaignId = await Dynalink.instance.getLastCampaignId();
Query attribution data on demand:
// By device fingerprint
final result = await Dynalink.instance.getAttributionByFingerprint(fingerprint);
// By DynaLink short code
final result = await Dynalink.instance.getAttributionByCode('SUMR99');
if (result != null && result.matched) {
print('gclid: ${result.gclid}');
print('fbclid: ${result.fbclid}');
print('attributed at: ${result.attributedAt}');
}
final url = await Dynalink.instance.createShortenedLink(
CreateDynalinkForm(
url: 'https://yourapp.io?screen=offer&id=99',
isDeepLink: true,
iosUrl: 'https://apps.apple.com/app/id123456789',
androidUrl: 'https://play.google.com/store/apps/details?id=com.yourapp',
fallbackUrl: 'https://yourapp.io',
campaignId: 4,
),
);
// List campaigns
final campaigns = await Dynalink.instance.getCampaigns(status: 'active');
// Get stats (last N days)
final stats = await Dynalink.instance.getCampaignStats(4, days: 7);
print('Total clicks: ${stats?.totalClicks}');
for (final day in stats?.clicksPerDay ?? []) {
print('${day.date}: ${day.count}');
}
// Create
final campaign = await Dynalink.instance.createCampaign(
CreateCampaignForm(name: 'Summer 2026', utmSource: 'google', utmMedium: 'cpc'),
);
// Update
await Dynalink.instance.updateCampaign(campaign.id, CreateCampaignForm(name: 'Summer 2026 — Revised'));
// Delete
await Dynalink.instance.deleteCampaign(campaign.id);
DynalinkEvent is emitted on dynamicLinkStream with the full payload. Values are also saved to SharedPreferences.The SDK logs every step. When nothing arrives on dynamicLinkStream, look for:
| Log line | Meaning | Fix |
|---|---|---|
fetchPendingLinkByCode(...) failed |
The short code could not be resolved | Wrong project key, link belongs to another project, or no connectivity |
Ignoring link that is not served by DynaLink |
The host is neither dynalink.app nor a declared custom domain |
Add the host to customDomains, or declare it on the project |
No link resolved for code "..." |
The API answered, but with nothing usable | Check the code exists in this project |
| (nothing at all, Android) | The App Link may not be verified, so nothing reaches the app | adb shell pm get-app-links {yourPackage} |
Three helpers replay each entry path against the real API, without needing a store install:
// Android install referrer / iOS clipboard — pass a URL carrying actual_url
await Dynalink.debugSimulateInstallReferrer(
'https://dynalink.app?dyna_code=SUMR99&actual_url=https%3A%2F%2Fyourapp.io&gclid=Cj0KCQj',
);
// iOS fingerprint matching — the fingerprint must already exist server-side
await Dynalink.debugSimulateIOSFingerprint('102.244.220.125-iOS-18.1-390x840');
// App already installed, opened by a verified App Link / Universal Link
await Dynalink.debugSimulateDeepLink('https://yourprefix.dynalink.app/SUMR99');