Usage
Detour delivers links to your app through a single provider. DetourProvider resolves the incoming link, whether deferred, Universal/App, or custom scheme, and useDetourContext() returns the result. You then navigate with your router, or navigate manually.
Provider setup
Section titled “Provider setup”import { DetourProvider, type Config } from '@swmansion/react-native-detour';
const config: Config = { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, shouldUseClipboard: true, // linkProcessingMode: 'all' // default};
export default function RootLayout() { return ( <DetourProvider config={config}> <RootNavigator /> </DetourProvider> );}For complete, runnable integrations of each navigation style, see Examples.
Handle resolved links
Section titled “Handle resolved links”import { useDetourContext } from '@swmansion/react-native-detour';import { useEffect } from 'react';import { useRouter } from 'expo-router';
export function RootNavigator() { const { isLinkProcessed, link, clearLink } = useDetourContext(); const router = useRouter();
useEffect(() => { if (!isLinkProcessed || !link) return;
router.replace({ pathname: link.pathname, params: link.params, });
clearLink(); }, [clearLink, isLinkProcessed, link, router]);
if (!isLinkProcessed) { return null; }
return <AppStack />;}link structure
Section titled “link structure”useDetourContext() returns a link object, or null when no link was detected.
-
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, or the raw value when it is not a URL.
-
link.type -
Where the link came from:
'deferred','verified', or'scheme'.
Use clearLink() after handling navigation to avoid repeated redirects.
Link processing modes
Section titled “Link processing modes”Config.linkProcessingMode controls which link sources the SDK handles:
| Value | Deferred | Universal / App | Custom scheme |
|---|---|---|---|
'all' (default) | Yes | Yes | Yes |
'web-only' | Yes | Yes | No |
'deferred-only' | Yes | No | No |
Use 'deferred-only' when Expo Router +native-intent.tsx already handles runtime and initial URLs.
Click limits
Section titled “Click limits”Before it delivers a Universal Link or App Link, the 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:
DetourProviderleaveslinkasnull, souseDetourContext()returns no route to navigate to.createDetourNativeIntentHandlerin resolve mode returnsfallbackPath.
Either way the SDK logs 🔗[Detour:CLICK_LIMIT_ERROR] with clicksInPeriod and effectiveLimit.
Other link types
Section titled “Other link types”This flow applies only to Universal Links and App Links.
- Deferred and short links — the same plan limits apply on the backend, but the SDK raises no click-limit error. An over-limit link fails to resolve and returns
null. - 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.
Expo Router integration
Section titled “Expo Router integration”Expo Router’s +native-intent.tsx intercepts and transforms incoming system URLs before the router processes them. createDetourNativeIntentHandler is a factory that builds such a handler, pre-configured for Detour link resolution.
Basic setup
Section titled “Basic setup”Create a +native-intent.tsx file in your app directory (e.g. src/app/+native-intent.tsx):
import { createDetourNativeIntentHandler } from "@swmansion/react-native-detour/expo-router";
export const redirectSystemPath = createDetourNativeIntentHandler();Resolve mode
Section titled “Resolve mode”To resolve Detour short links to their full destination URLs at the router level, pass a config object:
import { createDetourNativeIntentHandler } from "@swmansion/react-native-detour/expo-router";
export const redirectSystemPath = createDetourNativeIntentHandler({ config: { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, },});In resolve mode, when a user taps a Detour short link (e.g. https://abc123.godetour.link/xyz), the handler resolves it to the full destination URL and routes directly to the correct screen, all before the first render.
See Expo Router helper for the full list of options (custom hosts, fallback path, timeout, route mapping, etc.).
Custom domains
Section titled “Custom domains”The handler only processes URLs whose hostname matches its hosts list, which
defaults to [/\.godetour\.link$/i]. An app served from a
custom domain needs its own entry. Without
one, a Universal Link on links.yourdomain.com is passed to Expo Router
unhandled and Detour never processes it.
import { createDetourNativeIntentHandler } from "@swmansion/react-native-detour/expo-router";
export const redirectSystemPath = createDetourNativeIntentHandler({ hosts: ["links.yourdomain.com", /\.godetour\.link$/i], config: { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, },});Passing hosts replaces the default rather than adding to it. The example
lists both hosts, so the app keeps handling .godetour.link links as well. Keep
that second entry: if the custom domain is removed, or the paid plan lapses,
Detour serves the app’s links from .godetour.link again, and a build that
listed the custom domain alone ignores them.
Each entry is matched against the hostname alone, in one of three forms:
- an exact hostname:
"links.yourdomain.com" - a suffix wildcard:
"*.yourdomain.com", when you serve links from several subdomains - a regular expression:
/\.yourdomain\.com$/i
hosts is consulted only after the OS has decided to open your app, so the same
host has to be in the native configuration as well: associatedDomains on iOS,
an <intent-filter> on Android. See
Installation for
where each one goes.
Configure DetourProvider for native intent
Section titled “Configure DetourProvider for native intent”When using +native-intent.tsx to handle Universal Links and App Links, configure the provider to only process deferred links:
const config: Config = { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, linkProcessingMode: "deferred-only",};This avoids double-processing: +native-intent.tsx handles runtime and initial URLs, while DetourProvider handles deferred deep links only.
Advanced examples
Section titled “Advanced examples”The SDK repository covers two common native intent scenarios:
expo-router-native-intent— resolve mode with a custommapToRouteand scheme URL handling. Start here to transform resolved URLs before routing, for example to strip a path prefix, or to handle custom scheme links alongside Detour links.expo-router-advanced— Detour inside an auth- and onboarding-gated flow. Deferred and runtime links are preserved through sign-in and one-time onboarding before the target screen opens.
For all runnable examples, see Examples.
Alternative: catch-all route
Section titled “Alternative: catch-all route”If you prefer not to use +native-intent.tsx, keep linkProcessingMode: 'all' (the default) and add a catch-all route to your navigation stack.
DetourProvider then processes every link type: deferred, Universal/App, and custom scheme. Read the resolved link with useDetourContext() in a top-level component and navigate programmatically:
const { isLinkProcessed, link, clearLink } = useDetourContext();
useEffect(() => { if (!isLinkProcessed || !link) return; router.replace({ pathname: link.pathname, params: link.params }); clearLink();}, [isLinkProcessed, link]);This was the primary integration pattern before the SDK added native intent support (pre-1.x). It still works. +native-intent.tsx resolves links before the first screen renders.
React Navigation integration
Section titled “React Navigation integration”If you use React Navigation instead of Expo Router, the SDK provides a linking adapter for React Navigation’s custom linking API.
Detour resolves the incoming link, whether deferred, Universal/App, or scheme, and feeds the matched route to React Navigation. Navigation itself runs through your existing linking configuration.
The adapter needs a DetourProvider mounted above your NavigationContainer.
Linking setup
Section titled “Linking setup”import { DETOUR_LINKING_PREFIX, Detour } from "@swmansion/react-native-detour";
const linking = { prefixes: [DETOUR_LINKING_PREFIX], async getInitialURL() { return await Detour.getInitialURL(); }, subscribe(listener) { const subscription = Detour.addEventListener("url", ({ url }) => { listener(url); });
return () => subscription.remove(); }, config: { // your existing React Navigation screen-to-path mapping },};Pass this object to your navigation container:
<DetourProvider config={detourConfig}> <NavigationContainer linking={linking}> <RootNavigator /> </NavigationContainer></DetourProvider>The adapter does not pass the original URL to React Navigation. It rewrites every resolved link into an internal URL of its own, and that URL carries two additions:
- The
detour://prefix, exported asDETOUR_LINKING_PREFIX. React Navigation only parses URLs whose prefix it recognizes, which is why the constant belongs inprefixes. - Two query params,
fromDeepLink=trueandlinkType(deferred,verified, orscheme). They arrive as route params, so a screen can distinguish a deep-link entry from a normal one.
A resolved route /details?id=1 is therefore passed to React Navigation as:
detour://details?id=1&fromDeepLink=true&linkType=deferredYour config screen mapping is unchanged. It still matches on details.
Auth-gated deep links
Section titled “Auth-gated deep links”For apps that gate screens behind sign-in or onboarding, let React Navigation hold the deep link until the target screen becomes reachable. Render the screens conditionally on auth state, and opt in to React Navigation’s pending-link behavior on the navigator:
// Stack.Navigator only accepts Stack.Screen as a direct child,// so this is a render function rather than a component.function renderAuthScreens({ isSignedIn, isOnboardingCompleted }) { if (!isSignedIn) { return <Stack.Screen name="SignIn" component={SignIn} />; }
if (!isOnboardingCompleted) { return <Stack.Screen name="Onboarding" component={Onboarding} />; }
return ( <> <Stack.Screen name="Tabs" component={TabNavigator} /> <Stack.Screen name="Details" component={Details} /> </> );}
<Stack.Navigator UNSTABLE_routeNamesChangeBehavior="lastUnhandled"> {renderAuthScreens({ isSignedIn, isOnboardingCompleted })}</Stack.Navigator>;A deep link that arrives while the user is signed out is parsed, found unreachable because its target screen is not rendered yet, and remembered. Each time the rendered screen set changes, after sign-in and again after onboarding, React Navigation retries and opens the target screen.
For a complete setup, see the react-navigation-advanced example.
Analytics
Section titled “Analytics”Mounting DetourProvider turns analytics on. With the provider in place, it logs one event by itself: app_open, a retention event sent once per session on first mount (cold start). Universal Link and App Link opens are not client events. The backend records them through the click endpoint (see Click limits).
Everything else you log yourself, through DetourAnalytics. The provider sends every event it receives.
import { DetourAnalytics, DetourEventNames,} from "@swmansion/react-native-detour";
DetourAnalytics.logEvent(DetourEventNames.Purchase, { item: "subscription_premium", currency: "USD",});
DetourAnalytics.logRetention("home_screen_opened");Opting out of automatic events
Section titled “Opting out of automatic events”From 2.3.1 the app_open event can be turned off with
shouldTrackAutomaticEvents. Use it when the SDK must send no events until the
user has consented to tracking, for example under App Tracking Transparency on iOS, a
consent banner, or a regional privacy requirement:
const config: Config = { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, shouldTrackAutomaticEvents: false,};Only the automatic event is affected. DetourAnalytics.logEvent and
DetourAnalytics.logRetention still send, so you keep control over what is
reported and when. Link resolution is unchanged either way. Deferred and
runtime links resolve exactly as before.