SDK
React Native and Expo SDK
Add deep links, deferred deep links, install attribution and in-app events to a React Native or Expo app with react-native-appy and its config plugin.
react-native-appy is an Expo module over the native iOS SDK and Android SDK, with TypeScript types and a config plugin that sets up Associated Domains and Android App Links from app.json. It works in Expo apps and in bare React Native apps. The native guides go deeper on each topic; this page covers what is specific to React Native.
Requirements
- Expo SDK 54 or later. Bare React Native apps add the
expopackage withnpx install-expo-modules@latest. - iOS 15.1 or later, Android 5.0 (API 21) or later.
- A development build. Expo Go cannot load the native code.
- An app registered with Appy on the Enterprise plan, and its publishable key (
appy_pk_…).
Install the package
npx expo install react-native-appyRegister the app once under Apps & SDK in the dashboard or with POST /v1/apps: the Apple Team ID and bundle ID for iOS, the package name and every signing certificate’s SHA-256 fingerprint for Android, including Google Play’s app signing key and your EAS credentials. You get the publishable key and your link domain, such as acme.appy.to. See Register your app in the iOS guide and in the Android guide.
Add the config plugin
{
"expo": {
"scheme": "acme",
"plugins": [
["react-native-appy", { "linkDomains": ["acme.appy.to"] }]
]
}
}Run npx expo prebuild or an EAS build afterwards. The plugin adds applinks:acme.appy.to to the iOS entitlements, an autoVerify App Link intent filter to the main Android activity, the AppySDK pod to the Podfile from the iOS SDK’s Git repository (the CocoaPods trunk no longer accepts new pods), and Android backup rules that keep the first launch after a reinstall. The scheme lets in-app browsers that ignore Universal Links open your app through the Appy redirect page.
| Option | Default | Meaning |
|---|---|---|
linkDomains | required | Hosts of your Appy links. |
ios.developerMode | false | Also add applinks:<domain>?mode=developer, so development builds fetch the association file directly from Appy. |
android.excludeFromBackup | true | Exclude the SDK’s state from Auto Backup. If your app already sets backup rules, prebuild prints the line to add instead. |
Bare React Native
Without prebuild, add the Associated Domains capability and URL scheme in Xcode, the intent filters from Add intent filters and the backup rules from Keep the first launch a first launch, and this line inside your app target in ios/Podfile:
pod 'AppySDK', :git => 'https://github.com/appy-to/appy-ios-sdk.git', :tag => '1.0.0'No other native code is needed: the package receives links from the app delegate on iOS, including scene events that ExpoAppDelegate forwards, and from the React activity on Android.
Configure the SDK
Call configure once, at module scope of your entry file, such as app/_layout.tsx with Expo Router:
import Appy from 'react-native-appy';
Appy.configure({
publishableKey: 'appy_pk_live_…',
linkDomains: ['acme.appy.to'],
logLevel: __DEV__ ? 'debug' : 'error',
});| Option | Default | Meaning |
|---|---|---|
publishableKey | required | Your appy_pk_… key. |
linkDomains | [] | Hosts of your Appy links. The first one is used by buildLink. |
emailLinkDomains | [] | Click-tracking domains of your email tool that open the app. The native SDK resolves their links to the destination; see Links in marketing email. |
logLevel | 'error' | 'none', 'error', 'warning', 'info' or 'debug'. |
openDeferredDeepLinks | false | Let the SDK open a deferred link’s in-app URL itself. |
deferredDeepLinkTimeoutMs | 5000 | How long the first launch waits for Appy’s answer before reporting 'timeout'. |
launchLinkWaitMs | 500 | How long a launch waits for a link, so a cold start from a link sends one request. |
requestTimeoutMs | 10000 | Timeout per HTTP attempt. |
apiBaseUrl | https://api.appy.to | Change only for staging. |
android.collectInstallReferrer | true | Read the store’s install referrer on the first Android launch. |
android.metaAppId | none | Your Facebook App ID, to read the Meta Install Referrer. |
A link that opens the app before configure runs, such as the link of a cold start, is kept by the native SDK and resolved right after it. A second configure call in the same process, for example after a JavaScript reload, is ignored.
Handle deep links
import { useEffect } from 'react';
import { router } from 'expo-router';
import Appy from 'react-native-appy';
useEffect(() => {
const subscription = Appy.addDeepLinkListener((result) => {
if (result.status === 'found') {
const item = result.link.parameters.item;
router.push(item ? `/items/${item}` : `/campaigns/${result.link.slug}`);
}
});
return () => subscription.remove();
}, []);| Result | When |
|---|---|
{ status: 'found', link } | An Appy link opened the app, or on the first launch Appy found the link tapped before the install (link.isDeferred is true). |
{ status: 'notFound' } | First launch only. No Appy link led to this install. |
{ status: 'failed', error } | First launch only. 'timeout', 'network' or 'invalidResponse', for example an invalid key or a plan without the SDK. |
- The first launch after install calls the listener exactly once. Every Appy link that opens the app, cold or warm, arrives as
'found'. Other launches call nothing. - A result that arrives while no listener is registered is kept by the native SDK and delivered once to the next listener. Register it in a component that lives as long as the app, such as the root layout.
- A link has
url,deeplinkUrl,slug,parameters,isDeferredandclickedAt(aDate). When Appy cannot be reached, a link that opened the app still arrives, read from its own URL, withdeeplinkUrlandclickedAtset tonull. getLatestDeepLink()resolves to the most recent link, including a deferred link that arrived after the timeout.- Links from push notifications, in-app messages or QR scanners: pass the URL to
Appy.handleUrl(url). It resolves totruefor an Appy link, which then reaches the listener.
Expo Router and React Navigation
React Native’s Linking also receives every link that opens the app, so your router would try to open /spring for https://acme.appy.to/spring. Let the Appy listener route those links and tell the router to ignore them. With Expo Router, add app/+native-intent.tsx:
import { isAppyLink } from 'react-native-appy';
export function redirectSystemPath({ path }: { path: string; initial: boolean }) {
return isAppyLink(path, ['acme.appy.to']) ? null : path;
}isAppyLink is true for URLs on your link domains and for any URL with appy_click_id. Without the second argument it uses the link and email link domains passed to configure; with it, list your email link domains there too. With React Navigation, apply the same check in linking.getInitialURL and linking.subscribe.
Deferred deep links
On the first launch after install, the package asks Appy for the link tapped before the install. The listener receives 'found' with isDeferred === true, or 'notFound' for an organic install, within deferredDeepLinkTimeoutMs counted from configure. With openDeferredDeepLinks: true the SDK also opens the link’s deeplinkUrl, which your router receives through Linking; route deferred links in one of the two places, not both. Deferred deep links in the iOS guide explains timing and testing.
Install attribution
Appy.addAttributionListener((attribution) => {
analytics.setUserProperty('acquisition', attribution.isOrganic ? 'organic' : attribution.campaign ?? attribution.linkSlug);
});
const stored = await Appy.getAttribution();The attribution listener is called once, when Appy answers the first launch, also after a deep link timeout. getAttribution() resolves to the stored value on later launches. An attribution has isOrganic, linkSlug, linkTitle, source, medium, campaign, clickedAt and network for installs that Google Ads or Meta attributed. Apple Ads needs no setup; for Meta, set android: { metaAppId: … } in configure. Check attribution.verified from your backend before granting rewards, as described in Verified installs.
Track events
Appy.track('signup');
Appy.track('add_to_cart', { properties: { sku: 'A1', qty: 2 } });
Appy.track('purchase', { revenue: 19.98, currency: 'USD', deduplicationId: purchase.transactionId });Property values are strings, finite numbers or booleans; other values are dropped. Pass the store’s transaction ID as deduplicationId and Appy counts the purchase once, also when your backend reports it with the same ID. The limits and the sending schedule are in Track events.
Users, privacy and consent
Appy.setUserId('u_123');
if (!consent.hasAnswered) {
Appy.setTrackingEnabled(false);
}
Appy.configure(configuration);setUserId attaches an opaque user ID to later opens and events; pass null on sign-out. setTrackingEnabled(false) is stored across launches and stops the launch open, the deferred deep link lookup and events until you turn it back on, for example when the user consents. See Privacy and consent.
Build share links
const url = await Appy.buildLink('spring', { ref: referralCode });The result is https://acme.appy.to/spring?ref=… on the first link domain, or null before configure, without a link domain or for an invalid slug.
Test and debug
xcrun simctl openurl booted "https://acme.appy.to/spring?item=42"
adb shell 'am start -W -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "https://acme.appy.to/spring?item=42"'| Symptom | Check |
|---|---|
Cannot find native module 'Appy' | Expo Go or an old build. Make a development build after installing the package. |
| The router opens an unmatched route | Add +native-intent.tsx as shown above. |
| Links open the browser | The association files for your domain and the Team ID or SHA-256 fingerprint of the build you run. See Test and debug. |
{ status: 'failed', error: 'invalidResponse' } | HTTP 401 or 403 in the log, with logLevel: 'debug': the publishable key, or a plan without SDK access. |
API reference
| Function | Description |
|---|---|
configure(configuration) | Starts the SDK. Call once, early. |
addDeepLinkListener(listener) | Receives every AppyDeepLinkResult. Returns a subscription with remove(). |
addAttributionListener(listener) | Receives the install’s attribution, once per install. |
getAttribution(), getLatestDeepLink() | Stored attribution and the most recent link, or null. |
handleUrl(url) | Resolves an Appy link received another way. |
isAppyLink(url, linkDomains?) | Whether a URL is an Appy link, for routers. |
track(name, { properties, revenue, currency, deduplicationId }) | Queues an event. |
setUserId(userId) | Sets or clears the user ID. |
setTrackingEnabled(enabled), isTrackingEnabled() | Stored opt-out, on by default. |
buildLink(slug, parameters?) | Builds a share link. |
flush() | Sends queued events now. |
nativeSdkVersion(), pluginVersion | Versions of the native SDK and the package. |