Skip to content
Dashboard

Usage

Detour delivers links to your app through a single object. DetourService starts the native bridge, collects every resolved link, whether deferred, Universal/App, or custom scheme, into one pending intent, and notifies you when a link arrives. You read the intent, navigate, and mark it consumed.

DetourService is the recommended orchestration layer:

  • It configures the native bridge once.
  • It merges initial and runtime links into a single pending intent.
  • isInitialLinkProcessed tells you when the startup flow has finished.
  • consumePendingIntent() clears the intent explicitly. An intent stays pending until you clear it.
  • The same link arriving twice within 2 seconds is emitted once.
import 'package:detour_flutter_plugin/detour_flutter_plugin.dart';
import 'package:flutter/material.dart';
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final detour = DetourService();
@override
void initState() {
super.initState();
detour.addListener(_onDetourChanged);
_startDetour();
}
Future<void> _startDetour() async {
await detour.start(
const DetourConfig(
apiKey: '<REPLACE_WITH_YOUR_API_KEY>',
appID: '<REPLACE_WITH_APP_ID_FROM_PLATFORM>',
shouldUseClipboard: true,
linkProcessingMode: LinkProcessingMode.all,
),
);
}
void _onDetourChanged() {
final intent = detour.pendingIntent;
if (intent == null) return;
// Route once, then mark as consumed.
// context.go(intent.link.route);
detour.consumePendingIntent();
}
}

For a complete, runnable integration, see Examples.

DetourService extends ChangeNotifier. Every time a link resolves, listeners fire and pendingIntent holds the result until you consume it.

A DetourIntent carries three fields:

  • intent.link — the resolved link (see below)
  • intent.source — where it came from, one of initial, runtime, or manual
  • intent.receivedAt — when the service registered it

Call consumePendingIntent() after you navigate. Until you do, pendingIntent keeps returning the same intent, and a rebuild would route again.

link.route
Type String

Full in-app route, query string included: /details?utm=1.

link.pathname
Type String

Path without the query: /details.

link.params
Type Map<String, String>

Query params, already parsed.

link.url
Type String

The original URL.

link.type
Type LinkType

Where the link came from: deferred, verified, or scheme.

DetourConfig.linkProcessingMode controls which link sources the plugin handles:

ValueDeferredUniversal / AppCustom scheme
LinkProcessingMode.all (default)YesYesYes
LinkProcessingMode.webOnlyYesYesNo
LinkProcessingMode.deferredOnlyYesNoNo

Use deferredOnly when your navigation framework already handles runtime and initial URLs.

You can pass any URL to the plugin yourself:

final result = await detour.processLink(
'https://example.godetour.link/abc123?source=push',
);
if (result.link != null) {
// Navigate with result.link!.route
}

processLink returns a DetourResult with two fields:

  • processed — whether the native SDK ran the URL through link resolution
  • link — the resolved link, or null

The result is also pushed into the service as a pending intent. Pass emitIntent: false to inspect it without triggering your routing listener.

If you need full manual control, use DetourFlutterPlugin instead of DetourService. You are then responsible for configuration, the initial resolve, and the runtime stream.

final plugin = DetourFlutterPlugin();
await plugin.configure(
const DetourConfig(
apiKey: '<REPLACE_WITH_YOUR_API_KEY>',
appID: '<REPLACE_WITH_APP_ID_FROM_PLATFORM>',
),
);
final initial = await plugin.resolveInitialLink();
final stream = plugin.linkStream;
final processed = await plugin.processLink('https://example.godetour.link/path');

Before it delivers an App Link or Universal Link, the native SDK checks the click against your plan’s limit. Within the limit, links resolve normally.

The link is not delivered, and the plugin reports nothing. The native result comes back without a link, so DetourService registers no pending intent and your listener never fires. processLink() returns a DetourResult with processed: true and link: null, the same shape as a URL that had no match.

The only signal is in the native log, where the SDK writes [Detour:CLICK_LIMIT_ERROR] with clicksInPeriod and effectiveLimit:

Terminal window
adb logcat | grep -i detour # Android
xcrun simctl spawn booted log stream \
--predicate 'subsystem == "com.swmansion.detour"' # iOS simulator

This check applies only to App Links and Universal Links.

  • Deferred and short links — the same plan limits apply on the backend. On iOS no click-limit error is raised, an over-limit link fails to resolve, and link stays null. On Android an over-limit deferred match makes resolveInitialLink() raise NATIVE_ERROR with a [402] message. A short link opened as an App Link returns no link and no error.
  • Custom scheme links — resolved on device, never limit-checked.

For per-plan limits, pay-as-you-go overage, and how to raise them, see Billing & Payments.

Starting the service turns analytics on. With start() called, the native SDK logs one event by itself: app_open, a retention event sent once per session on cold start.

Everything else you log yourself:

await detour.logEvent(
DetourEventName.purchase,
data: {'value': 9.99, 'currency': 'USD'},
);
await detour.logRetention('home_screen_viewed');

logEvent takes a DetourEventName value, so you can log only the predefined event names. logRetention takes any string. The full enum is in the API Reference.