# Add device fingerprinting to a React Native app

> Add device fingerprinting to a React Native app with one TypeScript API over the native iOS and Android SDKs, then verify the result on your server.

Last updated: 2026-11-12

This tutorial adds **device fingerprinting** to a React Native app. `@fingerly/react-native` is a thin bridge to the native iOS and Android SDKs, because almost every signal worth having on a phone is out of reach of JavaScript. You write one TypeScript API, and each platform keeps its own identity rules.

## Before you start

- React Native 0.73 or newer, including the New Architecture, with iOS 13 and Android 5.0 (API 21) or newer.
- A development build. Expo Go cannot load native modules.
- A public key for the development environment, and a secret key for your server.
- A decision about consent: the SDK collects nothing until `consent` is `granted`.

## Add the SDK and verify the result

### Step 1: Create your keys

In the Fingerly dashboard, open **Integration > SDK keys** and create a **public** key in the **development** environment, and a **secret** key in the same environment for your server. Development keys run the same detection as production and cost nothing. A production key must also name your app, an Android package or an Apple app ID, as in [API keys and environments](https://docs.fingerly.io/docs/api-keys#native-apps).

### Step 2: Install the package

```bash npm
npm install @fingerly/react-native
cd ios && pod install
```

```bash pnpm
pnpm add @fingerly/react-native
cd ios && pod install
```

```bash yarn
yarn add @fingerly/react-native
cd ios && pod install
```

Autolinking handles the rest. On iOS, `pod install` downloads the native framework and checks it against a pinned checksum. On Android, the package adds Fingerly's Maven repository to the build itself.

### Step 3: Load the client and identify at sign-in

```ts fingerly.ts
import { load } from '@fingerly/react-native'

// consent: 'granted' only once you have consent (or another lawful basis)
const fingerly = await load({ apiKey: 'fly_pk_development_…', consent: 'granted' })

export async function signIn(email: string, password: string) {
  const { requestId } = await fingerly.identify({ tag: 'sign-in' })
  return api.signIn({ email, password, requestId })
}
```

Call `signIn` from a button's `onPress`. The consent state is shared by the whole app, so pass it on every `load`.

```tsx SignInScreen.tsx
import { signIn } from './fingerly'

<Button title="Sign in" onPress={() => signIn(email, password)} />
```

`identify` resolves with `requestId`, `visitorId`, `visitorIsNew`, `visitorConfidence`, `identifiable`, `duplicate`, `state`, `suspectScore`, `suspectLevel` and `report`. Several are `null` when the server did not say, which is not the same as `0` or `false`.

### Step 4: Verify on your server

A phone is a device you do not control, so the decision is never made in the app. Send the `requestId` to your backend with the action, read the stored event with your secret key, and check it belongs to this action and is recent.

```ts Sign-in handler (Node.js)
import { FingerlyAPIError, load } from '@fingerly/node'

const fingerly = load({ secretKey: process.env.FINGERLY_SECRET_KEY! })

export async function afterPasswordCheck(requestId: unknown) {
  if (typeof requestId !== 'string' || requestId === '') return 'second-factor' // missing evidence

  try {
    const event = await fingerly.events.get(requestId)
    if (event.tag !== 'sign-in') return 'second-factor'
    if (Date.now() - Date.parse(event.occurred_at) > 2 * 60 * 1000) return 'second-factor'
    return event.suspect_level === 'high' ? 'second-factor' : 'allow'
  } catch (error) {
    if (error instanceof FingerlyAPIError && error.code === 'event_not_found') return 'second-factor'
    throw error // still pending: answer with a retry, never a refusal
  }
}
```

Also check the request ID has not been used before, and answer a pending event with a retry rather than a refusal. [Server-side verification](https://docs.fingerly.io/docs/server-side-verification) explains each check, and the [use-case recipes](https://docs.fingerly.io/docs/use-cases) include a helper that does all of them.

### Step 5: Handle errors

```ts errors.ts
import { FingerlyError } from '@fingerly/react-native'

try {
  await fingerly.identify()
} catch (error) {
  if (error instanceof FingerlyError && error.status === 401) {
    // the key is wrong or revoked
  }
}
```

Retries happen in the native layer. Without consent, calls throw `ConsentError`, a `FingerlyError` whose `code` is `consent_required`. If the native module is missing, every call throws a `FingerlyError` that explains how to rebuild.

### Step 6: Run it on both platforms

Run a development build on a device or simulator for each platform, sign in, then open **Identification > Events** in the dashboard. Simulators and emulators record their own signals, which is expected there.

## What differs between iOS and Android

|  | iOS | Android |
| --- | --- | --- |
| The identity | A random install ID in the Keychain. | A random install ID in a file excluded from backups. |
| After a reinstall | The same visitor ID. | A new visitor ID. |
| Proof the report comes from your app | App Attest, once you add the capability and your Apple app ID to the key. | Hardware-backed attestation, which works without configuration. |
| `automatic` option | Joins an identification in flight and waits out a transient failure. | Accepted and has no effect yet. |

To remove the stored identity for an erasure request or account deletion, call `deleteStoredIdentity()`. See [account deletion and erasure](https://docs.fingerly.io/docs/app-store-privacy#account-deletion-and-erasure).

> **Warning:** Do not use the browser SDK in a React Native WebView. Inside an app it collects nothing and the answer has `identifiable: false`. Use this package.

## Common questions

### Does it work with Expo?

In a development build, yes. Expo Go cannot load native modules, so it cannot run this package.

### Does React Native fingerprint a device in JavaScript?

No. Collection happens in the native iOS and Android SDKs, and JavaScript gets typed results. That is why one TypeScript API can report what a pure JavaScript library cannot reach.

### Do I need CocoaPods for iOS?

Autolinking runs `pod install` for the bridge, which downloads the native framework and checks it against a pinned checksum. Your Podfile needs no entry for it.

### Is the result the same on iOS and Android?

The fields are the same. The identity rules differ: an iOS install ID survives a reinstall and an Android one does not. See [visitor identification](https://docs.fingerly.io/docs/visitor-identification#in-apps).

### How do I get the native SDK version?

Call `nativeVersion()`. It returns the version of the native SDK that did the collecting.

- [React Native SDK](https://docs.fingerly.io/docs/sdks/react-native): Every call, field and error.
- [iOS tutorial](https://docs.fingerly.io/docs/device-fingerprinting-ios): The native Swift side.
- [Android tutorial](https://docs.fingerly.io/docs/device-fingerprinting-android): The native Kotlin side.
- [Server-side verification](https://docs.fingerly.io/docs/server-side-verification): The four checks before you trust a result.
