Skip to content
Dashboard

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.

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.

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

useDetourContext() returns a link object, or null when no link was detected.

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 Record<string, string>

Query params, already parsed.

link.url
Type string | URL

The original URL, or the raw value when it is not a URL.

link.type
Type LinkType

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

Use clearLink() after handling navigation to avoid repeated redirects.

Config.linkProcessingMode controls which link sources the SDK handles:

ValueDeferredUniversal / AppCustom scheme
'all' (default)YesYesYes
'web-only'YesYesNo
'deferred-only'YesNoNo

Use 'deferred-only' when Expo Router +native-intent.tsx already handles runtime and initial URLs.

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.

The link is not delivered:

  • DetourProvider leaves link as null, so useDetourContext() returns no route to navigate to.
  • createDetourNativeIntentHandler in resolve mode returns fallbackPath.

Either way the SDK logs 🔗[Detour:CLICK_LIMIT_ERROR] with clicksInPeriod and effectiveLimit.

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’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.

Create a +native-intent.tsx file in your app directory (e.g. src/app/+native-intent.tsx):

src/app/+native-intent.tsx
import { createDetourNativeIntentHandler } from "@swmansion/react-native-detour/expo-router";
export const redirectSystemPath = createDetourNativeIntentHandler();

To resolve Detour short links to their full destination URLs at the router level, pass a config object:

src/app/+native-intent.tsx
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.).

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.

src/app/+native-intent.tsx
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.

The SDK repository covers two common native intent scenarios:

  • expo-router-native-intent — resolve mode with a custom mapToRoute and 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.

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.

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.

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 as DETOUR_LINKING_PREFIX. React Navigation only parses URLs whose prefix it recognizes, which is why the constant belongs in prefixes.
  • Two query params, fromDeepLink=true and linkType (deferred, verified, or scheme). 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=deferred

Your config screen mapping is unchanged. It still matches on details.

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.

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

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.