Expo SDK
Deep linking and mobile attribution for Expo apps. Pure JavaScript implementation -- no native code or ejecting required.
If you're using React Native CLI (not Expo), use the React Native SDK instead.
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 --
LinkFortyErrorwith 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',
});
Create Links
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
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.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
baseUrl | string | Yes | -- | Your LinkForty server URL |
apiKey | string | No | -- | API key for link creation and Cloud features |
appToken | string | No | -- | Public workspace token (at_…). Recommended for Cloud — enables organic-install attribution. Safe to ship in your app bundle. |
debug | boolean | No | false | Enable verbose logging |
attributionWindowHours | number | No | 168 | Attribution window in hours (1--2160) |
autoTrackNavigation | boolean | object | No | off | Auto-emit screen_view events from React Navigation. Requires navigationRef. See Automatic Screen Tracking. |
navigationRef | ref | No | -- | 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;
Link Creation
createLink(options)
Create a short link programmatically. Requires an API key.
await LinkForty.createLink(options: CreateLinkOptions): Promise<CreateLinkResult>
| Option | Type | Required | Description |
|---|---|---|---|
templateId | string | No | Template UUID (uses organization's default template if omitted) |
templateSlug | string | No | Template slug for URL construction |
deepLinkParameters | Record<string, string> | No | In-app routing parameters |
title | string | No | Link title |
description | string | No | Link description |
customCode | string | No | Custom short code |
utmParameters | UTMParameters | No | Campaign tracking parameters |
externalUserId | string | No | User 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
| Code | Trigger |
|---|---|
NOT_INITIALIZED | Method called before initialize() |
ALREADY_INITIALIZED | Duplicate initialize() call |
INVALID_CONFIGURATION | Invalid config parameters (e.g., non-HTTPS URL, out-of-range attribution window) |
NETWORK_ERROR | Failed network request |
INVALID_RESPONSE | Non-2xx server response |
DECODING_ERROR | Failed JSON parsing |
INVALID_EVENT_DATA | Empty event name or negative revenue amount |
MISSING_API_KEY | createLink() 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
queuedEventCountto 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
localhostand127.0.0.1for local development) - Attribution window must be between 1 and 2160 hours (90 days)
- Invalid configuration throws a
LinkFortyErrorwith codeINVALID_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
},
});
| Option | Type | Default | Description |
|---|---|---|---|
captureParams | string[] | [] (none) | Allow-list of route param keys to capture. Never list keys that can hold personal data. |
debounceMs | number | 350 | Debounce 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 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
Deep links not working in Expo Go
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
- Check
queuedEventCount-- events may be queued due to network issues - Call
flushEvents()to manually send queued events - Ensure the SDK has a valid install ID (check with
getInstallId())
Next Steps
- Create links in the dashboard
- View analytics for your app
- Configure attribution windows
- SDK Specification -- full feature reference