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.
Service setup
Section titled “Service setup”DetourService is the recommended orchestration layer:
- It configures the native bridge once.
- It merges initial and runtime links into a single pending intent.
isInitialLinkProcessedtells 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.
Handle resolved links
Section titled “Handle resolved links”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 ofinitial,runtime, ormanualintent.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 structure
Section titled “link structure”-
link.route -
Full in-app route, query string included:
/details?utm=1. -
link.pathname -
Path without the query:
/details. -
link.params -
Query params, already parsed.
-
link.url -
The original URL.
-
link.type -
Where the link came from:
deferred,verified, orscheme.
Link processing modes
Section titled “Link processing modes”DetourConfig.linkProcessingMode controls which link sources the plugin handles:
| Value | Deferred | Universal / App | Custom scheme |
|---|---|---|---|
LinkProcessingMode.all (default) | Yes | Yes | Yes |
LinkProcessingMode.webOnly | Yes | Yes | No |
LinkProcessingMode.deferredOnly | Yes | No | No |
Use deferredOnly when your navigation framework already handles runtime and initial URLs.
Manual processing
Section titled “Manual processing”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 resolutionlink— the resolved link, ornull
The result is also pushed into the service as a pending intent. Pass emitIntent: false to inspect it without triggering your routing listener.
Low-level API
Section titled “Low-level API”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');Click limits
Section titled “Click limits”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.
Over the limit
Section titled “Over the limit”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:
adb logcat | grep -i detour # Androidxcrun simctl spawn booted log stream \ --predicate 'subsystem == "com.swmansion.detour"' # iOS simulatorOther link types
Section titled “Other link types”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
linkstaysnull. On Android an over-limit deferred match makesresolveInitialLink()raiseNATIVE_ERRORwith 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.
Analytics
Section titled “Analytics”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.