Skip to main content

Expo SDK

Deep linking and mobile attribution for Expo apps. Pure JavaScript implementation -- no native code or ejecting required.

React Native CLI?

If you're using React Native CLI (not Expo), use the React Native SDK instead.

Faster setup with AI

Using Claude Code, Cursor or Codex? Copy this prompt and paste it into your agent from the root of your project. It installs the SDK, wires deep links through your router (asking you which screen each link should open), adds one event, and verifies the install in your LinkForty dashboard.

Install the LinkForty SDK in an Expo app
Expo SDK install, deferred and direct deep links through Expo Router, and a first in-app event.
Show the prompt
I use LinkForty (https://linkforty.com), a deep-linking and mobile attribution platform: short links that route each click to the app, the app store or the web, and record clicks, installs and in-app events per link.

Before writing any code, read the LinkForty documentation index at https://docs.linkforty.com/llms-full.txt and the pages listed under "Docs" below. Work from what the docs say, not from assumptions about how similar products work. If something you need is not in the docs or in this prompt — an ID, a token, a URL — stop and ask me for it rather than inventing a placeholder that looks real.

## Task: install the LinkForty Expo SDK

Add the LinkForty SDK to this app so that an install that follows a click on one of my links is attributed to that link, a link's deep-link path opens the right screen on first launch (deferred deep linking) and on later opens (Universal Links / App Links), and one key in-app event is recorded.

## Values

- **App token**: shown in the LinkForty dashboard under Workspace Settings → App Token (it starts with `at_`). Ask me for it; put it in the app's config or environment, not hard-coded in a committed source file.
- **Base URL**: `https://api.linkforty.com` for LinkForty Cloud. (Only a self-hosted LinkForty server uses a different origin — ask if you are not sure which this is.)
- **Link host**: the host my links are on, shown in the dashboard under Settings → Domains — the workspace's own host (it looks like `something.toapp.me`) and any verified custom domain listed there. Ask me for it. This is the domain the app must claim for Universal Links / App Links.
- **LinkForty MCP server** (optional): if it is configured in this environment (https://mcp.linkforty.com/mcp, or the `linkforty-mcp` command), use `get_app_config` to read the workspace's app settings and `create_link` to make a test link at the end. Do not set it up for me if it is not.

## How to work

1. **Look before you change anything.** Identify the framework, its version, the navigation/router library and where the app initializes at launch. List the screens (routes) you found, and **ask me which screen each incoming link should open and what parameter it needs** — for example "a link with `productId` opens the product screen". Wait for my answer before editing; do not guess routes.
2. **Work on a new git branch.** If the working tree has uncommitted changes, stop and tell me instead of building on top of them.
3. **Follow the docs pages named below, not memory.** Package names, method names and config keys come from those pages; LinkForty's SDKs are not the same as other attribution SDKs you may know.
4. **Do not invent values.** Anything in "Values" that says "ask me" is something you ask for. Never commit a token to source.
5. **Keep the app working without a link.** A launch with no matching click must behave exactly as before: no crash, no error surfaced to the user, no blocked start-up.
6. **Finish with a summary**: every file you changed and why; the manual steps that remain for me (Apple Team ID and bundle ID, Play signing SHA-256 fingerprint, store listing, a rebuild and reinstall) and where in the LinkForty dashboard each one goes (Workspace Settings → App Configuration); and anything you were unsure about.
7. **Verify.** Run the app once, then check the LinkForty dashboard: the SDK Doctor badge in the sidebar (https://docs.linkforty.com/guides/attribution/sdk-health) should report the SDK as installed. If the MCP server is available, create a test link with `create_link` and open it on a device or simulator; the click and the deep link should reach the screen we agreed on.

## Steps

1. **Install** with `npx expo install @linkforty/mobile-sdk-expo expo-device expo-application expo-localization expo-linking @react-native-async-storage/async-storage` (https://docs.linkforty.com/sdks/expo#installation). Note: deep links do not work in Expo Go; a development build is needed to test them.
2. **Initialize once at launch** in the root layout or App component: `await LinkForty.initialize({ baseUrl, appToken, debug: __DEV__ })` from `@linkforty/mobile-sdk-expo`, with the base URL and app token from "Values" (https://docs.linkforty.com/sdks/expo#initialize-the-sdk). Leave `apiKey` out — it is only for creating links from the app.
3. **Claim the link host** so the OS opens the app instead of the browser (https://docs.linkforty.com/sdks/expo#platform-configuration): add `applinks:<link host>` to `ios.associatedDomains` and an `android.intentFilters` entry with `autoVerify: true` for `https://<link host>` in `app.json` / `app.config.*`, then regenerate the native projects if this app prebuilds. LinkForty serves the AASA and assetlinks.json files on that host itself; nothing to upload. Tell me the Apple Team ID / bundle ID and Android package / signing SHA-256 must be entered in LinkForty under Workspace Settings → App Configuration, or the OS will not verify the domain.
4. **Deferred deep link**: register `LinkForty.onDeferredDeepLink((data) => …)` right after initialize. When `data` is non-null, route to the screen we agreed on using its `deepLinkPath` / `customParameters`; when it is null (organic install) do nothing (https://docs.linkforty.com/sdks/expo#deep-linking).
5. **Direct deep links**: register `LinkForty.onDeepLink((url, data) => …)` and route the same way. Use the app's existing Expo Router / React Navigation navigation for both — do not add a second navigation system.
6. **One in-app event**: at the obvious success moment in this app (sign-up completed, purchase, or the first key action — tell me which you chose) call `LinkForty.trackEvent('signup')` or the purchase equivalent with amount and ISO currency (https://docs.linkforty.com/sdks/expo#track-events). No personal data in properties.

## Docs

- Expo SDK: https://docs.linkforty.com/sdks/expo (Installation, Platform Configuration, Initialize the SDK, Deep Linking, Track Events)
- SDK integration overview: https://docs.linkforty.com/guides/attribution/sdk-integration
- Deferred deep linking: https://docs.linkforty.com/guides/attribution/deferred-deep-linking
- Link hosts and app association files: https://docs.linkforty.com/guides/links/custom-domains#your-linkforty-domain
- SDK Doctor: https://docs.linkforty.com/guides/attribution/sdk-health

## Done when

- Opening one of my links on a device without the app, installing it and launching it lands on the screen we agreed on, and the install appears under Installs in the LinkForty dashboard attributed to that link.
- Opening the same link with the app installed goes straight to that screen without a browser stop.
- The in-app event shows under Events, attached to that install.
- SDK Doctor in the dashboard sidebar reports the SDK as installed.

When you are done, tell me: exactly which files changed and why, how I can verify it worked end to end (what to tap or run, and what I should see in the LinkForty dashboard), and anything you were unsure about.

It needs your app token; the prompt asks you for it, and the MCP server can supply it if you have that set up.

Features​

  • Deferred deep linking -- route new users to specific content after install
  • Direct deep linking -- handle links when the app is already installed
  • Install attribution -- probabilistic fingerprint matching
  • Event tracking -- log in-app events tied to attribution data
  • Revenue tracking -- structured purchase tracking with dedicated method
  • Offline event queue -- events persist across app restarts and retry automatically
  • Programmatic link creation -- create short links from your app
  • Typed errors -- LinkFortyError with specific error codes
  • Pure Expo modules -- uses expo-device, expo-application, expo-localization, expo-linking
  • No native code required -- works with Expo Go and EAS builds

Requirements​

  • Expo SDK 50+
  • React Native >= 0.73.0

Installation​

npx expo install @linkforty/mobile-sdk-expo expo-device expo-application expo-localization expo-linking @react-native-async-storage/async-storage

Platform Configuration​

Universal Links (iOS) and App Links (Android) need the app to claim your link host. In Expo that is app config, not native project files — app.json or app.config.*:

{
"expo": {
"ios": {
"associatedDomains": ["applinks:acme.toapp.me"]
},
"android": {
"intentFilters": [
{
"action": "VIEW",
"autoVerify": true,
"data": [{ "scheme": "https", "host": "acme.toapp.me" }],
"category": ["BROWSABLE", "DEFAULT"]
}
]
}
}
}

Use your LinkForty domain as the host, and add an entry for each custom domain. Your LinkForty server automatically hosts the Apple App Site Association and Digital Asset Links files on every one of those hosts. Rebuild after changing this (npx expo prebuild if you manage native projects, or an EAS / development build) — Expo Go cannot claim domains.


Quick Start​

Initialize the SDK​

import { LinkForty } from '@linkforty/mobile-sdk-expo';
import { useEffect } from 'react';

function App() {
useEffect(() => {
async function setup() {
await LinkForty.initialize({
baseUrl: 'https://go.yourdomain.com',
apiKey: 'your-api-key', // optional -- required for link creation
appToken: 'at_your_app_token', // recommended for Cloud -- enables organic-install attribution
debug: __DEV__,
attributionWindowHours: 168, // 7 days (default)
});

// Handle deferred deep links (new installs)
LinkForty.onDeferredDeepLink((data) => {
if (data) {
console.log('Attributed install:', data.shortCode);
navigateToContent(data);
}
});

// Handle direct deep links (app already installed)
LinkForty.onDeepLink((url, data) => {
console.log('Deep link opened:', url);
if (data) {
navigateToContent(data);
}
});
}

setup();
}, []);

return (/* your app */);
}

Track Events​

import { LinkForty } from '@linkforty/mobile-sdk-expo';

// Custom event
await LinkForty.trackEvent('add_to_cart', {
productId: '123',
category: 'electronics',
});

// Revenue event
await LinkForty.trackRevenue(29.99, 'USD', {
productId: '123',
productName: 'Wireless Headphones',
});
import { LinkForty } from '@linkforty/mobile-sdk-expo';

const result = await LinkForty.createLink({
deepLinkParameters: { route: 'PRODUCT', id: '123' },
title: 'Check out this product',
utmParameters: { source: 'app', medium: 'share' },
});

console.log(result.url); // https://go.yourdomain.com/abc123
console.log(result.shortCode); // abc123
console.log(result.linkId); // uuid
warning

Link creation requires an API key. The simplified endpoint (/api/sdk/v1/links) is only available on LinkForty Cloud, not self-hosted Core.


API Reference​

Initialization​

initialize(config)​

Initialize the SDK. Must be called before any other method.

await LinkForty.initialize(config: LinkFortyConfig): Promise<InstallAttributionResponse | null>

Returns the install attribution response on first launch, or null on subsequent launches.

ParameterTypeRequiredDefaultDescription
baseUrlstringYes--Your LinkForty server URL
apiKeystringNo--API key for link creation and Cloud features
appTokenstringNo--Public workspace token (at_…). Recommended for Cloud — enables organic-install attribution. Safe to ship in your app bundle.
debugbooleanNofalseEnable verbose logging
attributionWindowHoursnumberNo168Attribution window in hours (1--2160)
autoTrackNavigationboolean | objectNooffAuto-emit screen_view events from React Navigation. Requires navigationRef. See Automatic Screen Tracking.
navigationRefrefNo--Your React Navigation container ref. Required when autoTrackNavigation is enabled.

isInitialized​

Boolean getter indicating whether the SDK has been initialized.

if (LinkForty.isInitialized) { /* ... */ }

Deep Linking​

onDeferredDeepLink(callback)​

Register a callback for deferred deep links. If data is already available, the callback fires immediately.

LinkForty.onDeferredDeepLink(callback: (data: DeepLinkData | null) => void): void

onDeepLink(callback)​

Register a callback for direct deep links. The SDK resolves Universal Links / App Links server-side for enriched data.

LinkForty.onDeepLink(callback: (url: string, data: DeepLinkData | null) => void): void

handleDeepLink(url)​

Manually pass a URL for the SDK to process.

LinkForty.handleDeepLink(url: string): void

Event Tracking​

trackEvent(name, properties?)​

Track a custom in-app event.

await LinkForty.trackEvent(name: string, properties?: Record<string, any>): Promise<void>

trackRevenue(amount, currency, properties?)​

Track a revenue event with structured amount and currency.

await LinkForty.trackRevenue(
amount: number,
currency: string,
properties?: Record<string, any>
): Promise<void>

Event Queue​

flushEvents()​

Manually send all queued events.

await LinkForty.flushEvents(): Promise<void>

clearEventQueue()​

Remove queued events without sending them.

await LinkForty.clearEventQueue(): Promise<void>

queuedEventCount​

Getter returning the number of events waiting to be sent.

const count = LinkForty.queuedEventCount;

createLink(options)​

Create a short link programmatically. Requires an API key.

await LinkForty.createLink(options: CreateLinkOptions): Promise<CreateLinkResult>
OptionTypeRequiredDescription
templateIdstringNoTemplate UUID (uses organization's default template if omitted)
templateSlugstringNoTemplate slug for URL construction
deepLinkParametersRecord<string, string>NoIn-app routing parameters
titlestringNoLink title
descriptionstringNoLink description
customCodestringNoCustom short code, up to 20 characters; must not already be used in your workspace (409 SHORT_CODE_TAKEN)
utmParametersUTMParametersNoCampaign tracking parameters
externalUserIdstringNoUser ID for deduplication and share attribution

User Identity​

setExternalUserId(id)​

Set the external user ID for share attribution. This ID is automatically attached to all createLink() calls unless overridden per-call. Pass null to clear.

LinkForty.setExternalUserId(id: string | null): void

getExternalUserId()​

Returns the current external user ID, or null if not set.

LinkForty.getExternalUserId(): string | null

Data Access​

getInstallId()​

await LinkForty.getInstallId(): Promise<string | null>

getInstallData()​

await LinkForty.getInstallData(): Promise<DeepLinkData | null>

isFirstLaunch()​

await LinkForty.isFirstLaunch(): Promise<boolean>

Data Management​

clearData()​

Wipe all locally stored SDK data.

await LinkForty.clearData(): Promise<void>

reset()​

Return the SDK to an uninitialized state so it can be re-initialized.

LinkForty.reset(): void

Error Handling​

All errors are LinkFortyError instances with a .code property for programmatic handling:

import { LinkForty, LinkFortyError, LinkFortyErrorCode } from '@linkforty/mobile-sdk-expo';

try {
await LinkForty.createLink({ title: 'Test' });
} catch (error) {
if (error instanceof LinkFortyError) {
switch (error.code) {
case LinkFortyErrorCode.MISSING_API_KEY:
console.log('API key required for link creation');
break;
case LinkFortyErrorCode.NOT_INITIALIZED:
console.log('Call initialize() first');
break;
case LinkFortyErrorCode.NETWORK_ERROR:
console.log('Network unavailable');
break;
}
}
}

Error Codes​

CodeTrigger
NOT_INITIALIZEDMethod called before initialize()
ALREADY_INITIALIZEDDuplicate initialize() call
INVALID_CONFIGURATIONInvalid config parameters (e.g., non-HTTPS URL, out-of-range attribution window)
NETWORK_ERRORFailed network request
INVALID_RESPONSENon-2xx server response
DECODING_ERRORFailed JSON parsing
INVALID_EVENT_DATAEmpty event name or negative revenue amount
MISSING_API_KEYcreateLink() called without an API key

Offline Resilience​

Events are automatically queued in AsyncStorage when the network is unavailable:

  • Maximum queue size: 100 events
  • Queue persists across app restarts
  • Events are retried automatically when connectivity is restored
  • Use flushEvents() to manually trigger a send
  • Use clearEventQueue() to discard queued events without sending
  • Check queuedEventCount to monitor queue depth

TypeScript Types​

import { LinkFortySDK, LinkFortyError, LinkFortyErrorCode } from '@linkforty/mobile-sdk-expo';

import type {
LinkFortyConfig,
DeepLinkData,
InstallAttributionResponse,
UTMParameters,
DeviceFingerprint,
CreateLinkOptions,
CreateLinkResult,
EventRequest,
DeferredDeepLinkCallback,
DeepLinkCallback,
} from '@linkforty/mobile-sdk-expo';

Configuration Validation​

The Expo SDK validates configuration at initialization:

  • Base URL must use HTTPS (except localhost and 127.0.0.1 for local development)
  • Attribution window must be between 1 and 2160 hours (90 days)
  • Invalid configuration throws a LinkFortyError with code INVALID_CONFIGURATION

Automatic Screen Tracking​

Enable autoTrackNavigation to emit a screen_view event on every React Navigation transition automatically — no per-screen trackEvent calls. Screen views flow through the normal event pipeline and carry the active deep-link attribution context, feeding App Analytics and the SDK health signal.

It requires a navigation container ref:

import { createNavigationContainerRef, NavigationContainer } from '@react-navigation/native';
import { LinkForty } from '@linkforty/mobile-sdk-expo';

export const navigationRef = createNavigationContainerRef();

await LinkForty.initialize({
baseUrl: 'https://go.yourdomain.com',
appToken: 'at_your_app_token',
autoTrackNavigation: true, // screen names only — privacy-safe default
navigationRef,
});

<NavigationContainer ref={navigationRef}>
{/* ...screens... */}
</NavigationContainer>

Capturing route params​

autoTrackNavigation: true captures screen names only. To capture specific, non-PII params, pass an options object with an explicit allow-list:

await LinkForty.initialize({
baseUrl: 'https://go.yourdomain.com',
navigationRef,
autoTrackNavigation: {
captureParams: ['productId', 'category'], // only these keys
debounceMs: 350, // default 350ms
},
});
OptionTypeDefaultDescription
captureParamsstring[][] (none)Allow-list of route param keys to capture. Never list keys that can hold personal data.
debounceMsnumber350Debounce window for rapid transitions, in milliseconds.

What gets sent​

Each transition emits a screen_view event with screen (active route name), previousScreen (when available), and params (only allow-listed keys). Rapid transitions are debounced and duplicates dropped. If autoTrackNavigation is enabled without a navigationRef, screen tracking is disabled (a warning is logged in debug mode).

Expo Router

Expo Router is built on React Navigation. Use its navigation container ref so the SDK can observe route changes.


Self-Hosted Configuration​

For self-hosted LinkForty Core, omit the API key:

await LinkForty.initialize({
baseUrl: 'https://links.yourcompany.com',
debug: false,
});

Troubleshooting​

Expo Go has limitations with Universal Links / App Links. For full deep linking support, use a development build or EAS Build.

ALREADY_INITIALIZED error​

The SDK guards against double initialization. If you need to reinitialize (e.g., after changing configuration), call reset() first:

LinkForty.reset();
await LinkForty.initialize(newConfig);

Events not appearing in analytics​

  1. Check queuedEventCount -- events may be queued due to network issues
  2. Call flushEvents() to manually send queued events
  3. Ensure the SDK has a valid install ID (check with getInstallId())

Next Steps​

Resources​