Flutter SDK


Installation

Add the dependency to your pubspec.yaml:

dependencies:
  dynalink_flutter: ^0.0.18
flutter pub get

Platform Setup

Android

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.

Heads up — Do not enable Flutter's own deep linking. If your manifest declares <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.

iOS

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.

Initialize

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.

Handling Deep Link Events

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!);
});

App Links & Direct Opens

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:

  • Always navigate with event.actualUrl. Never parse the incoming URL path yourself — on the direct path it only contains the short code.
  • The destination is resolved over the network on that path, so a device with no connectivity gets no event. The SDK logs the failure.
Tip — Verify the Android side with adb shell pm get-app-links {yourPackage}. A domain in the verified state means links now open the app directly.

DynalinkEvent Fields

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

Persisted Attribution Getters

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();

Attribution API

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}');
}

Creating Links

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,
  ),
);

Campaign Management

// 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);

How Attribution Works

  1. Click — User clicks a DynaLink. The fingerprint page captures click IDs from the URL and stores them against the device fingerprint.
  2. Redirect — User is sent to the App Store or Play Store. On Android, the original DynaLink URL is embedded as the install referrer.
  3. Install & Open — User installs and opens the app. The SDK runs the attribution flow:
    • Android — reads install referrer URL → parses code → fetches click IDs from attribution API.
    • iOS (clipboard) — if the DynaLink URL is in the clipboard, processes it the same way.
    • iOS (fingerprint) — regenerates the device fingerprint → finds the pending link → marks it as confirmed → fetches click IDs from the attribution API.
    • App already installed (App Link / Universal Link) — the OS opens the app straight away: the SDK resolves the code through the links API, reports the click, and reads any click IDs from the attribution API.
  4. Event — A DynalinkEvent is emitted on dynamicLinkStream with the full payload. Values are also saved to SharedPreferences.

Troubleshooting

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');