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

Already using Claude Code, Cursor, or Claude Desktop? The LinkForty MCP server can scaffold your SDK integration automatically. Just ask: "Help me add the LinkForty SDK to my Expo app."

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

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