React Native SDK
Track events, identify users, manage consent and run personalization in React Native apps on iOS and Android.
React Native SDK
The Intempt React Native SDK wraps the native iOS and Android SDKs behind one JavaScript API. Events, identity, consent, delivery and personalization all run natively — this package is the bridge, not a second implementation.
Requirements
| React Native | 0.76 or later |
| iOS | 15.1 or later |
| Android | API 24 or later |
| Dependencies | none |
Installation
npm install @intempt-technologies/react-nativecd ios && pod installAutolinking registers both native modules. No manual linking, and nothing to add to MainApplication or AppDelegate.
Initialization
Credentials are passed in code. Call init() once, as early as possible — before the first screen renders, so session tracking starts at app launch rather than at first navigation.
import { init } from '@intempt-technologies/react-native';
const intempt = await init({
apiKey: 'yourPrefix.yourSecret',
orgId: 'your-org',
projectId: 'your-project',
sourceId: 'your-source',
});| Field | Type | Description |
|---|---|---|
apiKey | string | API key in prefix.secret form |
orgId | string | Organization identifier |
projectId | string | Project identifier |
sourceId | string | Source the events are attributed to |
instanceName | string | Optional. Defaults to "default" |
init() resolves to an instance. Every method on it returns a Promise.
On Android these credentials are passed to the native SDK at runtime, the same as on iOS. The package bundles intempt-android 5.0.0.
Multiple instances
Each named instance has its own credentials, queue and identity.
iOS only for now. The Android SDK is a singleton, so any instanceName other than "default" rejects with unsupported_on_android.
const eu = await init({ ...config, instanceName: 'eu' });
const us = await init({ ...config, instanceName: 'us' });Tracking events
await intempt.track('Viewed pricing', { source: 'nav', seats: 3, trial: false });Property values may be strings, numbers, booleans, null, Date, arrays or nested objects.
Numbers and booleans stay numbers and booleans on both platforms. Nested maps and arrays keep their structure.
What the return value means
track() resolves to whether the event was accepted into the queue, not whether it reached Intempt.
const queued = await intempt.track('Signed up');false means the event was dropped before queueing — the user is opted out, a property could not be represented, encoding failed, or storage was unavailable.
On Android the value is the native SDK's own acceptance result, the same as on iOS.
To confirm delivery, flush and read the count:
const delivered = await intempt.flush();Whether these calls build a profile depends on the source, not the SDK. On a Users and events source they resolve identity and update the user and account. On an Events only source they build no profile: its events are kept as raw events with no user or account. You pick this when you create the source in the console. See What a source collects.
Identifying users
await intempt.identify('user-123', {
userAttributes: { email: 'ada@example.com', plan: 'pro' },
});Associate a profile with an account:
await intempt.group('acct-9', {
accountAttributes: { tier: 'enterprise', seats: 40 },
});Record an event against a user, an account, or both:
await intempt.record('Renewed', {
userId: 'user-123',
accountId: 'acct-9',
data: { mrr: 120 },
});Reading the current identity
const profileId = await intempt.getProfileId();
const sessionId = await intempt.getSessionId();Ending a session
await intempt.logOut(); // rotate the anonymous identity, keep queued events
await intempt.reset(); // rotate the identity AND discard queued eventsUse logOut() when a user signs out on a device others may use — it stops the next person inheriting the previous identity. Use reset() when queued events should not be sent at all.
Commerce events
await intempt.productView('sku-1');
await intempt.productAdd('sku-1', 2);
await intempt.productOrdered([
{ productId: 'sku-1', quantity: 2 },
{ productId: 'sku-2', quantity: 1 },
]);An entry missing productId or quantity fails the whole call rather than being skipped, so a dropped line item never quietly changes an order total.
Consent
import { ConsentAction } from '@intempt-technologies/react-native';
await intempt.consent(ConsentAction.Accept, 1798761600, {
email: 'ada@example.com',
category: 'marketing',
});| Argument | Type | Description |
|---|---|---|
action | ConsentAction | Accept or Reject |
validUntil | number | Unix seconds the decision is valid until |
email | string | Optional |
message | string | Optional |
category | string | Optional |
Three behaviours worth knowing:
- Consent is transmitted even when the user is opted out. A withdrawal has to reach Intempt.
- It is sent immediately to its own endpoint, not batched with events.
Rejectopts the user out andAcceptopts them in. You do not need a separateoptOut()call.
Opting users out
await intempt.optOut();
await intempt.optIn();
const optedIn = await intempt.isOptedIn();
const optedOut = await intempt.hasOptedOut();optOut() stops collection and discards events already queued. Events gathered before someone objected are not uploaded afterwards. Queued consent records are kept — they are the record of the decision itself.
Delivery
const delivered = await intempt.flush();
await intempt.setFlushInterval(30); // seconds; 0 disables the timer
const interval = await intempt.getFlushInterval();Automatic events
await intempt.setAutomaticEvents({
sessions: true,
versionChanges: false,
appStateChanges: false,
});| Option | Default | Emits |
|---|---|---|
sessions | on | Session start and end, with device attributes |
versionChanges | off | Application Installed / Application Updated, once per version |
appStateChanges | off | Application Opened / Application Backgrounded on every transition |
Only sessions are on by default. Turn the others on deliberately — appStateChanges in particular fires on every foreground and background transition.
Autocapture
How this compares across SDKs: Autocapture and automatic events.
Off on both platforms until you start it. It hooks the native view layer, so it is never installed uninvited.
What a user types into a text field is never captured, on either platform. A field's name, a switch's state and a button's label still come through.
await intempt.autocapture.configure({ screenViews: true, controlInteractions: true });
await intempt.autocapture.start();
await intempt.autocapture.stop();On Android this needs intempt-android 5.0.0 or later. The React Native SDK bundles intempt-android 4.0.1 by default, and 4.x starts autocapture at init(). Set intemptAndroidVersion in your gradle.properties to pick up 5.0.0 until a React Native release bundles it.
Recommendations
const products = await intempt.products({
feedId: 'homepage-feed',
count: 10,
fields: ['productId', 'title', 'price', 'imageUrl', 'url'],
});Experiment and personalization assignment is not part of the mobile SDKs — it is an intemptjs capability. Recommendation feeds are a different thing and are here.
fields defaults to a compact set on purpose, and you should widen it deliberately rather than omit it. A request with no fields returns every catalog column, including raw ML embedding vectors — for the same ten products that is 222,919 bytes against 503, roughly 443 times the payload, over whatever connection the device happens to have.
Push notifications
await intempt.setPushToken(hexToken);
await intempt.trackPushOpen(notification.data);
await intempt.trackPushReceived(notification.data);iOS — pass the APNs device token as a hex string. Binary data has no representation across the React Native bridge.
Android — token registration requires Google Play Services. An emulator running the default system image does not have them, and registration fails there in a way that is hard to read. Use a google_apis image when testing push.
Error handling
Every rejection is an IntemptError carrying a code.
import { IntemptError, IntemptErrorCode } from '@intempt-technologies/react-native';
try {
await intempt.track('Checkout started');
} catch (error) {
if (error instanceof IntemptError && error.isRetryable) {
// transport failure or a 5xx; error.retryAfter may be set
}
}| Code | Meaning |
|---|---|
malformed_api_key | Key is not in prefix.secret form |
missing_configuration | An identifier was blank |
invalid_property_value | A property value could not be represented |
missing_identity | A required identifier was absent for the event type |
encoding_failed | The payload could not be serialized |
terminal | Will not succeed on retry |
retryable | Retry with backoff |
transport | Network layer failed |
storage_unavailable | The queue could not persist |
server | Intempt rejected the request with detail |
not_initialized | Called before init() |
unknown | Native returned a code this package version does not recognise — the two have drifted |
A 401 is classified terminal, not retryable — a bad credential cannot start working on retry. Queued events are kept, because the data is fine and the credentials are what need fixing.
Platform differences
A method that a platform does not support yet rejects with unsupported_on_android or unsupported_on_ios and names the method. It never resolves silently.
if (error.isUnsupported) {
// present on the contract, not on this platform yet
}Until Android SDK 3.0, these reject on Android:
reset · getProfileId · getSessionId · flush · getFlushInterval · setFlushInterval · products · getAutomaticEvents · setAutomaticEvents · the whole autocapture object (configure, start, stop, isRunning) · setPushToken · trackPushOpen · trackPushReceived · init() with any instanceName other than "default".
TypeScript
Types ship with the package; nothing extra to install.
import type {
IntemptConfig,
IntemptProperties,
ProductRecommendation,
} from '@intempt-technologies/react-native';Related
Feature flags
Read a flag by key. You never name a mode — Intempt resolves whether that key is an experiment, a personalization or a flag, so a key can change mode without your code changing.
const on = await sdk.boolVariation('new_checkout', { userId: 'u-1' }, false);
const all = await sdk.allFlags({ userId: 'u-1' });| Call | Returns |
|---|---|
variation | just the value |
allFlags | every flag for this person, in one call |
The default is required. It is what you receive on a network failure, a timeout, an unknown key or a malformed response — so an outage degrades to your existing behaviour instead of an exception.
Evaluation is remote. There is no local rule engine and no flag store to poll, so bucketing can never disagree between your code and the platform.
Typed helpers
variation returns whatever the flag holds. When you know the shape, these return it already typed and fall back to the default if the served value is the wrong type — so a misconfigured flag degrades instead of throwing.
| Call | Returns |
|---|---|
boolVariation | a boolean |
stringVariation | a string |
numberVariation | a number |
There is no JSON helper here yet — use variation and parse the value yourself. The Node, Python, PHP and Swift SDKs do have one.
Every Intempt SDK exposes these same three calls and hits the same endpoint,
optimization/choose-api — there is no separate web, mobile or backend endpoint. What the
console labels server-side means the value is read by an SDK rather than authored in the
visual editor; it is not a statement about which machine runs your code.
See the feature flags guide for rollout, off values and key locking.
