Initialize and handle links
Flutter SDK
Configure the SDK with DectogConfig, listen to the link stream, handle the cold-start link once, and understand which URLs the SDK recognizes, filters and reports.
Initialize
Section titled “Initialize”FlutterDectog() returns a singleton. Call initialize once, early in your app, with an SDK key:
import 'package:flutter_dectog/flutter_dectog.dart';
final dectog = FlutterDectog();
await dectog.initialize(DectogConfig( apiKey: 'dtg_pub_your_sdk_key',));That is the whole setup for the hosted service: the SDK talks to https://app.dectog.com and recognizes links on {username}.dectog.com.
What initialize does, in order:
- Loads the persisted SDK state (customer id, the
enabledflag, install id). - Starts the event queue and sends the device ping (the first initialize of each UTC day), unless the user opted out.
- Subscribes to incoming links and processes the link that launched the app, while at the same time registering the install if this is the first launch. The call resolves when both are done.
- Schedules the launch-time late claims (a referrer that was not definitive, an Apple Search Ads token that was not ready) in the background.
Rules:
- A second
initializethrows aDectogErrorof typeconfiguration(“FlutterDectog is already initialized”). Concurrent calls during the first initialization share it. - A failed
initializethrowsDectogError(“Failed to initialize: …”) and leaves the SDK uninitialized, so a corrected config can be retried without a restart. - Every other method throws a
configurationerror untilinitializesucceeded.willShowPasteboardPrompt()is the exception; it works beforeinitialize. - On the first launch,
initializereturns only after the install registration finished: the referrer wait (up toinstallReferrerTimeout), the pasteboard read (up to 6 seconds when the user has to answer the iOS alert) and up to two install requests of 15 seconds each. Do not block your first frame on the returned future. Start it, build the UI, and react to the streams.
DectogConfig
Section titled “DectogConfig”| Field | Type | Default | Meaning |
|---|---|---|---|
apiKey |
String |
required | An SDK key (dtg_pub_…, Settings → SDK keys). Every authenticated call is signed with it. |
baseUrl |
String? |
null → https://app.dectog.com |
API base URL. Set it only when self-hosting. Trailing slashes are stripped. |
linkRootDomain |
String? |
null → dectog.com when baseUrl is unset, else the host of baseUrl |
The domain tenant links hang off ({username}.{root}/{slug}). Set it when self-hosted links live on a different host than the API. |
linkDomains |
List<String> |
[] |
Your custom link domains, like go.yourbrand.com: https://go.yourbrand.com/{slug} is a link of your account. The SDK also learns the active domain from the server after its first answer; listing it covers a link that opens the app on its very first start. |
enableDebugLogging |
bool |
false |
Prints SDK activity with print. |
installReferrerProvider |
Future<String?> Function()? |
null |
Replaces the built-in Android referrer readers with your own Play referrer string. |
installReferrerTimeout |
Duration |
5 seconds | How long the first-launch registration waits for the Android install referrer. Android only. |
collect |
Set<DectogCollection>? |
null → the device tier on; the pasteboard (iOS) and the OEM referrers as the dashboard sets them; no Apple Search Ads token; the advertising id when a provider is set |
What this build collects: deviceTier, pasteboard, oemReferrers, appleSearchAdsToken, adId. A set is exact. See Device data and privacy. |
advertisingIdProvider |
Future<DectogAdvertisingId?> Function()? |
null |
Reads the IDFA or GAID; pass DectogAdId.provider from flutter_dectog_adid. |
fbAppId |
String? |
null → the Facebook App ID saved in the dashboard |
Your Facebook App ID. Enables Meta’s on-device install referrer on Android. |
persistDeviceId |
bool |
true |
Keep the first iOS IDFV in the Keychain so the device id survives reinstall. |
enabled |
bool |
true |
Whether collection is on before the app calls setEnabled. false waits for consent; see Waiting for consent. |
environment |
DectogEnvironment? |
null |
The environment this build’s key should belong to, live or test. diagnostics() warns when the key belongs to the other one. See Testing. |
The config also exposes DectogConfig.defaultBaseUrl (https://app.dectog.com), DectogConfig.defaultLinkRootDomain (dectog.com), the resolved getters effectiveBaseUrl and effectiveLinkRootDomain, and readClipboardFor(isIOS:). toString() masks the API key.
The attribution options are explained on Install attribution, Deterministic attribution and Device data and privacy.
Settings from the dashboard
Section titled “Settings from the dashboard”initialize also reads the app’s settings from the server (GET /api/v1/sdk-config): the Facebook App ID, the OEM store referrers switch and the pasteboard setting saved on the Apps page, the app’s custom schemes and identifiers, and the hosts its links live on. They apply wherever the corresponding DectogConfig field is left null, so a change in the dashboard reaches installed apps without a release. The SDK caches the answer for an hour: a launch with a fresh cache makes no request, the very first launch waits at most 1.5 seconds before registering the install, and a stale cache is refreshed in the background.
FlutterDectog().diagnostics() compares the build with that record and returns warnings, also printed with enableDebugLogging: a bundle ID or package that differs from the dashboard’s, an iOS URL scheme missing from CFBundleURLTypes, and on Android 12 and newer every link host the system has not verified for App Links. It never stops the SDK; call it from a debug screen or a test build.
Self-hosted backends
Section titled “Self-hosted backends”Set baseUrl to your API and, when the links live on a different host, linkRootDomain:
await dectog.initialize(DectogConfig( apiKey: 'dtg_pub_your_sdk_key', baseUrl: 'https://app.example.com', linkRootDomain: 'example.com', // links at https://{username}.example.com/{slug}));Against a local worker use http://10.0.2.2:8787 on the Android emulator and http://localhost:8787 on the iOS simulator, with no linkRootDomain (the local worker runs in path mode, /r/{username}/{slug}). See Testing.
Which URLs the SDK recognizes
Section titled “Which URLs the SDK recognizes”An incoming URL is a DecTog link when it matches one of these forms. baseHost is the host of effectiveBaseUrl, root is effectiveLinkRootDomain (or baseHost when that is null).
| Form | Example | Used for |
|---|---|---|
https://{username}.{root}/{slug} |
https://acme.dectog.com/launch |
Hosted and self-hosted subdomain links. The username must contain no dot, and the host must differ from baseHost. |
https://{domain}/{slug}, for a host in linkDomains or the custom domain the server names |
https://go.yourbrand.com/launch |
Your custom link domain. The link belongs to the key’s own account, which the SDK reads from the server. {domain}/r/{username}/{slug} works too. A host equal to baseHost or under root is never a custom domain. |
{baseHost}/r/{username}/{slug} |
http://localhost:8787/r/acme/launch |
Path mode (local development, path-mode deployments). |
any host containing localhost with ?user={username}/{slug} |
https://localhost:3000?user=acme/launch |
Development query form. |
any non-http(s) scheme whose query carries dtg_l={username}/{slug} |
myapp://products/42?dtg_click=…&dtg_l=acme/launch |
Custom-scheme opens and the Android intent:// bounce. The host and path are the app’s own route; the link comes from dtg_l. |
dectog://{username}/{slug} |
dectog://acme/launch |
Development only: the dectog scheme without a tag. |
For https forms only the first path segment is read as the slug; the rest of the path, the query and the fragment are kept on originalUrl. URLs that match none of these forms are still emitted (see below) but carry an error.
Handle links
Section titled “Handle links”Two entry points deliver the same objects:
onLinkClick, a broadcast stream ofDectogLinkClickfor links that arrive while the app runs and for the cold-start link.getInitialLink(), a future with the link that launched the app, ornull.
dectog.onLinkClick.listen(_handleLink);
final initial = await dectog.getInitialLink();if (initial != null) _handleLink(initial);
void _handleLink(DectogLinkClick click) { if (click.error != null) { // Not a DecTog link, or the SDK was not initialized yet. return; } // click.username and click.slug come from the URL itself. // click.linkResolution is set when the public resolve call succeeded. final route = click.clickParams?.deepLink // dec_deeplink on the opened URL ?? click.linkResolution?.deepLinkPath // the link's stored in-app route ?? click.linkResolution?.target; // the link's web target (may be null) _navigate(slug: click.slug, route: route);}What a DectogLinkClick carries:
| Field | Set when |
|---|---|
originalUrl, receivedAt |
Always. |
username, slug |
Parsed from the URL; 'unknown' when parsing failed. |
linkResolution |
The public GET /api/v1/resolve/{username}/{slug} answered. It is null when the request failed; the click and the open were still reported. |
error |
The URL is not a DecTog link (Invalid DecTog URL format), processing failed, or the SDK was not initialized. |
clickParams (getter) |
The URL carries dec_* parameters. deepLink is the raw dec_deeplink value as it appeared in the URL. |
isResolved, targetUrl, linkType (getters) |
Convenience views on linkResolution. |
The DectogLinkClickX extension adds queryParameters, queryParameter(key), path, host, scheme and fragment, all read from originalUrl. DectogLinkResolution is described on the API reference; its target is null when the link inherits the app’s fallback URL.
The cold-start link is consumed once
Section titled “The cold-start link is consumed once”The link that launched the app is processed and emitted once per process, however it is delivered:
- Android delivers the launch URL on both
getInitialLink()and the link stream; the SDK folds the two into oneDectogLinkClickobject, soonLinkClickemits once andgetInitialLink()returns the same object. - A later
getInitialLink()call (from another screen, for example) returns the same click again without logging anything. - An identical URL delivered twice within 10 seconds while the app runs is treated as one open.
- On Android, a task relaunched from the recents screen replays the original intent. The SDK detects
FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY, ignores that URL, andgetInitialLink()returnsnullfor that launch.
Sensitive URLs are never clicks
Section titled “Sensitive URLs are never clicks”OAuth and social-login callbacks that open the app through app_links are neither reported nor emitted:
- Schemes matching
fb<digits>,li<digits>,pdk<digits>,twitterkit-…,com.googleusercontent.apps.<digits>-…, or any scheme containingoauth. - Any URL whose query or fragment carries
access_token,id_tokenoroauth_token.
What the SDK reports for an open
Section titled “What the SDK reports for an open”For every recognized link the SDK sends, without blocking your handler:
POST /api/v1/clickswith the slug,is_mobile_app: true, platform, language,client_click_time(when the URL reached the device), the opened URL’sutm_*anddec_*values, the device id and model with the device tier, and on Android the launching package (launch_referrer). When the URL carriesdtg_clickfrom anintent://bounce, the server converts the existing web click row (if it is less than 24 hours old) instead of inserting a second one.POST /api/v1/app-opens, which feeds the per-link Opens count andlastOpenon attribution re-pulls. Skipped when the user opted out.
Neither call is retried, and a failure never affects the emitted click.
Links tapped inside your app
Section titled “Links tapped inside your app”iOS and Android never hand a Universal Link or an App Link back to the app that shows it. A DecTog link in your own chat, feed or help screen therefore opens the browser when tapped. Pass it to the SDK instead:
// In your own link tap handler:final click = await FlutterDectog().handleUrl(url);if (click == null) { await launchUrl(Uri.parse(url)); // not a DecTog link: open it as usual}handleUrl routes the URL exactly like a link that opened the app: onLinkClick emits it (once, with inApp: true, so your existing handler takes it), the 10-second deduper and the sensitive-URL filter apply, and it returns the same DectogLinkClick. It counts as an app open, not as a click: the Opens count goes up, the Clicks count does not. It returns null for a URL that is not a DecTog link (including a custom domain the SDK does not know) and for an authentication callback.
Complete example
Section titled “Complete example”A small service that initializes the SDK, routes opens and reacts to install attribution. _openRoute stands for your navigation.
import 'dart:async';
import 'package:flutter_dectog/flutter_dectog.dart';
class DectogService { DectogService._(); static final DectogService instance = DectogService._();
final FlutterDectog _dectog = FlutterDectog(); StreamSubscription<DectogLinkClick>? _links; StreamSubscription<DectogAppInstallResponse>? _installs;
Future<void> start({required String apiKey}) async { _links = _dectog.onLinkClick.listen(_onLink); _installs = _dectog.onAppInstalled.listen(_onInstall); await _dectog.initialize(DectogConfig( apiKey: apiKey, enableDebugLogging: true, )); // The cold-start link was already delivered on onLinkClick; calling // getInitialLink() here returns the same object and logs nothing twice. }
void _onLink(DectogLinkClick click) { if (click.error != null) return; final route = click.clickParams?.deepLink ?? click.linkResolution?.deepLinkPath ?? click.slug; _openRoute(route); }
void _onInstall(DectogAppInstallResponse install) { // Route on any candidate (medium confidence or better) ... final route = install.routing?.deepLink ?? install.deepLink; if (route != null) _openRoute(route); // ... but only credit the link when the server granted credit. if (install.matched) { _creditReferral(install.slug!, install.clickParams?.campaign); } }
void _openRoute(String route) { // Push the matching screen with your router. }
void _creditReferral(String slug, String? campaign) { // Your own bookkeeping. }
Future<void> stop() async { await _links?.cancel(); await _installs?.cancel(); await _dectog.dispose(); }}onAppInstalled is a replaying stream: a listener that subscribes after the first launch finished still receives the latest verdict, and currentAppInstall reads it synchronously. The verdict fields are explained on Install attribution.
dispose() cancels the subscriptions, closes both streams, flushes the event queue and closes the HTTP client. After dispose() the SDK cannot be initialized again in the same process, so call it only when the app shuts down.