> For the complete documentation index, see [llms.txt](https://docs.tryterra.co/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.tryterra.co/mobile-sdks/react-native-sdk-2.md).

# React Native SDK

Install @tryterra/react-native, connect Apple Health, Health Connect or Samsung Health, and keep their data flowing to Terra in the background.

This page shows how to use `@tryterra/react-native` 2.x to connect Apple Health on iOS, and Health Connect or Samsung Health on Android: install the package, create the client, connect a user and set up background delivery.

{% hint style="info" %}
This page covers `@tryterra/react-native` 2.x. For `terra-react` 1.x, read [React Native SDK 1.x](/mobile-sdks/react-native.md). To move an existing app, read [React Native upgrade guide](/mobile-sdks/react-native-migrating-to-2.md).
{% endhint %}

## Before you begin

* React Native 0.81 or later, with the New Architecture.
* iOS 15.1 or later.
* Android `minSdkVersion` 28 and `compileSdkVersion` 36.
* A backend endpoint that mints Terra auth tokens. See [Generate an auth token](#generate-an-auth-token).

## Install the SDK

1. Install the [`@tryterra/react-native` package](https://www.npmjs.com/package/@tryterra/react-native) using npm (`npm install @tryterra/react-native@2.2.1`). The package carries the iOS SDK and resolves the Android SDK from Maven Central, so there is nothing else to install.
2. Complete the following iOS and/or Android setup.

{% tabs %}
{% tab title="iOS Setup" %}

1. In your terminal, cd to your `/ios` folder, and run `pod install` to install all the dependencies.
2. **Add Capabilities** in **Signing & Capabilities**:
   1. HealthKit > HealthKit Background Delivery
   2. Background Modes > Background fetch
3. **Add the following keys to your Info.plist:**

**Option 1: add the keys in Xcode.**

1. Go to the `/ios` directory of your project, open the `.xcworkspace` in Xcode.
2. Go to **Info.plist**, and add the following keys and values:

| Key                                             | Value                                                                            |
| ----------------------------------------------- | -------------------------------------------------------------------------------- |
| `TerraDevID`                                    | Your **Developer ID** provided by Terra                                          |
| Privacy - Health Share Usage Description        | Why your app reads health data                                                   |
| Privacy - Health Update Usage Description       | Why your app writes health data. App Store Connect rejects the upload without it |
| Permitted background task scheduler identifiers | `co.tryterra.sdk.refresh`                                                        |

**Option 2: edit Info.plist directly.**

{% code title="Info.plist" lineNumbers="true" %}

```xml
<key>TerraDevID</key>
<string>YOUR_DEV_ID</string>
<key>NSHealthShareUsageDescription</key>
<string>We read your health data to show your activity and sleep.</string>
<key>NSHealthUpdateUsageDescription</key>
<string>We save your workouts to Apple Health.</string>
<key>UIBackgroundModes</key>
<array>
  <string>fetch</string>
</array>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
  <string>co.tryterra.sdk.refresh</string>
</array>
```

{% endcode %}

4. **Bootstrap the SDK at launch.** Call `TerraReactNative.bootstrap()` first thing in `application(_:didFinishLaunchingWithOptions:)`, before React Native starts:

{% code title="AppDelegate.swift" lineNumbers="true" %}

```swift
import TerraReactNative

func application(
  _ application: UIApplication,
  didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
  TerraReactNative.bootstrap()
  // ... start React Native as usual
  return true
}
```

{% endcode %}

{% hint style="info" %}
The SDK client is built here, natively, because a background wake from HealthKit starts your app without a JavaScript runtime, and iOS only accepts a background task registered before launch finishes. `bootstrap()` reads `TerraDevID` from `Info.plist`, builds the client and registers background sync. Calling it more than once does nothing.

Without it, every call rejects with `bridge.not_bootstrapped`, and the error's `details.line` names the line to add.
{% endhint %}
{% endtab %}

{% tab title="Android Setup" %}

1. **Add your Developer ID** inside `<application>`:

{% code title="android/app/src/main/AndroidManifest.xml" lineNumbers="true" %}

```xml
<meta-data
  android:name="co.tryterra.sdk.DEV_ID"
  android:value="YOUR_DEV_ID" />
```

{% endcode %}

There is no code to add. The package's `androidx.startup` initializer builds the client before `Application.onCreate`. If your app removes WorkManager's default initializer to configure WorkManager itself, call `TerraReactNative.bootstrap(this)` at the end of `Application.onCreate` instead.

2. **Declare the Health Connect permissions.** Add one `<uses-permission>` for each Health Connect type your `permissions.request` reads. For `read: ['activity', 'daily', 'sleep']`:

{% code title="android/app/src/main/AndroidManifest.xml" lineNumbers="true" %}

```xml
<uses-permission android:name="android.permission.health.READ_HEART_RATE" />
<uses-permission android:name="android.permission.health.READ_RESTING_HEART_RATE" />
<uses-permission android:name="android.permission.health.READ_HEART_RATE_VARIABILITY" />
<uses-permission android:name="android.permission.health.READ_STEPS" />
<uses-permission android:name="android.permission.health.READ_SLEEP" />
<uses-permission android:name="android.permission.health.READ_EXERCISE" />
<uses-permission android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED" />
<uses-permission android:name="android.permission.health.READ_TOTAL_CALORIES_BURNED" />
<uses-permission android:name="android.permission.health.READ_BASAL_METABOLIC_RATE" />
<uses-permission android:name="android.permission.health.READ_DISTANCE" />
<uses-permission android:name="android.permission.health.READ_FLOORS_CLIMBED" />
<uses-permission android:name="android.permission.health.READ_ELEVATION_GAINED" />
<uses-permission android:name="android.permission.health.READ_POWER" />
<uses-permission android:name="android.permission.health.READ_SPEED" />
<uses-permission android:name="android.permission.health.READ_OXYGEN_SATURATION" />
<uses-permission android:name="android.permission.health.READ_RESPIRATORY_RATE" />
<uses-permission android:name="android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND" />
<uses-permission android:name="android.permission.health.READ_HEALTH_DATA_HISTORY" />

<queries>
  <package android:name="com.google.android.apps.healthdata" />
</queries>
```

{% endcode %}

{% hint style="warning" %}
Health Connect refuses the whole permission sheet if the request asks for one type the manifest does not declare. The [Android SDK guide](/mobile-sdks/android-sdk-2.md) lists the types behind every category.
{% endhint %}

3. **Add the permission rationale.** Health Connect links to your privacy policy from its permission screen. Add the rationale filter to your main activity, and the alias that Android 14 and later require:

{% code title="android/app/src/main/AndroidManifest.xml" lineNumbers="true" %}

```xml
<activity android:name=".MainActivity" android:exported="true">
  <!-- ... your existing filters ... -->
  <intent-filter>
    <action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
  </intent-filter>
</activity>

<activity-alias
  android:name=".ViewPermissionUsageActivity"
  android:exported="true"
  android:targetActivity=".MainActivity"
  android:permission="android.permission.START_VIEW_PERMISSION_USAGE">
  <intent-filter>
    <action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
    <category android:name="android.intent.category.HEALTH_PERMISSIONS" />
  </intent-filter>
</activity-alias>
```

{% endcode %}

{% hint style="warning" %}
Without the alias, Android 14 and later refuse every read.
{% endhint %}
{% endtab %}
{% endtabs %}

## Initialize the SDK

The first step is to **create the Terra client.**

Create it **once**, **at module scope** (e.g. in a `terra.ts` file), and import it wherever you need it. The native side of the SDK is already running from [Install the SDK](#install-the-sdk), so the client only connects your JavaScript to it.

### Import the Terra SDK

{% code title="terra.ts" lineNumbers="true" %}

```typescript
import { createTerra } from '@tryterra/react-native';
```

{% endcode %}

### Create the Terra client

Call `createTerra()` with the following options:

* `tokenProvider`: A function that returns a promise of a **one-time authentication token** from your backend. See [Generate an auth token](#generate-an-auth-token) below.
* `tokenTimeoutMs` (optional): How long the native side waits for `tokenProvider`. Defaults to 30 seconds. After that the connect rejects with `bridge.token_timeout` on iOS and `token_provider_failed` on Android.

Find more details in the SDK Reference: [createTerra](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/react-native-sdk-2)

{% code title="terra.ts" lineNumbers="true" %}

```typescript
import { createTerra } from '@tryterra/react-native';

export const terra = createTerra({
  tokenProvider: () => api.terraToken(), // your backend's token endpoint
});
```

{% endcode %}

## Connect a user

Once the client exists, you can create a connection by calling `connect()`, then request permissions with `permissions.request()` to show the permission screen.

### Call `connect()`

Import `EndUser` from `@tryterra/react-native` and call [`connect()`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/react-native-sdk-2) with the following arguments:

* `source`: `'health'` (Apple Health on iOS, Health Connect on Android) or `'samsungHealth'` (Samsung Health directly, Android only).
* `endUser`: The person the connection is for. `EndUser.reference(id)` with the same value your backend sends as `reference_id`, or `EndUser.anonymous` if your backend sends none.
* `options.token` (optional): A token you already hold, for example from a QR code or a deep link. A token passed this way always creates a fresh link.

### Call `permissions.request()`

Call [`permissions.request()`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/react-native-sdk-2) with the categories to read and write. The categories are `activity`, `body`, `daily`, `sleep` and `nutrition`. With SDK 2.2.1 or later you can also request `menstruation`, and `athlete` on iOS. `connect` never shows a permission screen on its own.

{% tabs %}
{% tab title="Apple Health (iOS)" %}
{% code title="ConnectButton.tsx" lineNumbers="true" %}

```tsx
import { EndUser, TerraError } from '@tryterra/react-native';
import { terra } from './terra';

const connectHealth = async () => {
  try {
    await terra.connect('health', EndUser.reference(user.id));
    await terra.permissions.request({ read: ['activity', 'body', 'daily', 'sleep'] });
  } catch (error) {
    if (error instanceof TerraError) {
      console.error('Connection failed:', error.code);
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="Health Connect (Android)" %}
{% code title="ConnectButton.tsx" lineNumbers="true" %}

```tsx
import { EndUser, TerraError } from '@tryterra/react-native';
import { terra } from './terra';

const connectHealth = async () => {
  try {
    await terra.connect('health', EndUser.reference(user.id));
    const granted = await terra.permissions.request({
      read: ['activity', 'body', 'daily', 'sleep'],
    });
    console.log('Granted:', granted);
  } catch (error) {
    if (error instanceof TerraError) {
      console.error('Connection failed:', error.code);
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="Samsung Health (Android)" %}
Follow [Samsung Health](/mobile-sdks/samsung-health.md) first, then set `terraSamsungHealth=true` in `android/gradle.properties`.

{% code title="ConnectButton.tsx" lineNumbers="true" %}

```tsx
import { EndUser, TerraError } from '@tryterra/react-native';
import { terra } from './terra';

const connectSamsung = async () => {
  try {
    await terra.connect('samsungHealth', EndUser.reference(user.id));
    await terra.permissions.request({ read: ['daily', 'sleep'] }, 'samsungHealth');
  } catch (error) {
    if (error instanceof TerraError) {
      console.error('Connection failed:', error.code);
    }
  }
}
```

{% endcode %}

Without `terraSamsungHealth=true`, `'samsungHealth'` rejects with `health_data_unavailable`. To read Samsung Health's data through Health Connect instead, connect `'health'`, once the person lets Samsung Health share with Health Connect.
{% endtab %}
{% endtabs %}

### What `permissions.request()` resolves with

* **Android**: what the sheet granted, narrowed to this call. Check it rather than assuming the person said yes.
* **iOS**: `undefined`. iOS never reveals whether a read was granted.

When the request reads anything, the Android sheet also asks for background read and history read, if your manifest declares them. Check them afterwards with `terra.permissions.backgroundReadStatus()` and `terra.permissions.historyReadStatus()`.

{% hint style="info" %}
**Heart rate needs `body` on iOS.** Heart rate is uploaded on the daily route. On iOS its permission comes with `body`, so request `body` to upload it. On Android it comes with `daily`.
{% endhint %}

### The same person keeps their link

A stored link is restored only for the person it was made for. A normal launch for the same person restores it with no network call and never calls `tokenProvider`. When someone else signs in on the same phone, the SDK retires the old link and links again, so their data does not arrive under the first person's `reference_id`.

## Generate an auth token

`tokenProvider` needs a **single-use token** to link the device to Terra. It is called only while `connect` makes a new link: the first time the device links, after a reinstall, when a different person connects, or when Terra no longer accepts the device's key.

Generate the token with `POST https://api.tryterra.co/v2/auth/generateAuthToken` from **your backend server**, with your Terra `x-api-key` and `dev-id` in the headers and your own user identifier as `reference_id`. Then return it to your app from your own API.

{% code title="Your backend" %}

```bash
curl --request POST \
  --url 'https://api.tryterra.co/v2/auth/generateAuthToken?reference_id=<YOUR_USER_ID>' \
  --header 'dev-id: <YOUR_DEV_ID>' \
  --header 'x-api-key: <YOUR_API_KEY>'
```

{% endcode %}

{% hint style="warning" %}

* Tokens expire after three minutes and are spent on first use.
* **DO NOT** expose your API key in your mobile app. Make this call **only** from your **backend**.
  {% endhint %}

## Validate the connection

To check that a connection is still working on the client side, use [`diagnostics.state()`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/react-native-sdk-2). `terra.connections()` lists the connections on the device, with the `userId` of each.

{% code title="app.tsx" lineNumbers="true" %}

```typescript
const state = await terra.diagnostics.state();        // { state: 'healthy' }, { state: 'quiet', since }, ...
const last = await terra.diagnostics.lastDelivery();   // when data last reached Terra
const stop = terra.diagnostics.events((event) => console.log(event));
```

{% endcode %}

{% hint style="info" %}
**Always validate the connection on app start.** If `diagnostics.state()` is `{ state: 'linkRevoked' }`, Terra stopped accepting this device's key. Call `connect` again with the same end user to link again.

`diagnostics.events` reports everything the SDK does as it happens, and never carries a health value. Call the returned function to stop listening.
{% endhint %}

## Set up background delivery

Background delivery allows data to be synced even when your app is not brought to the foreground. There is no call that starts a sync: after the permission sheet is answered, the SDK uploads on its own, in the foreground and in the background.

{% tabs %}
{% tab title="Apple Health" %}
Background sync is registered by `TerraReactNative.bootstrap()` from [Install the SDK](#install-the-sdk). Check that:

1. `bootstrap()` runs in `didFinishLaunchingWithOptions`.
2. `co.tryterra.sdk.refresh` is in `BGTaskSchedulerPermittedIdentifiers`.
3. The HealthKit Background Delivery capability is on.

`terra.background.isRegistered()` answers whether it was registered.
{% endtab %}

{% tab title="Android (Health Connect & Samsung)" %}
Background sync is scheduled through WorkManager by the package's startup initializer. Keep WorkManager's default initializer, or call `TerraReactNative.bootstrap(this)` at the end of `Application.onCreate`.

Declare `READ_HEALTH_DATA_IN_BACKGROUND` in your manifest so the permission sheet asks for background read. `terra.background.isRegistered()` answers whether background sync was registered.
{% endtab %}
{% endtabs %}

From here, health data events reach your data destination, such as a webhook, automatically.

You can also [request historical data](/unified-api/requesting-historical-data.md) from your backend to backfill.

Check out the [React Native SDK reference](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/react-native-sdk-2) for details about all the functions in the SDK.

## Read and write on the device

Local reads go to Apple Health or Health Connect and never touch the network. Each takes a `TimeRange`:

{% code lineNumbers="true" %}

```typescript
const heartRate = await terra.body.heartRate({ kind: 'lastHours', hours: 6 });
const steps = await terra.daily.steps({ kind: 'today' });
const sleep = await terra.sleep.segments({ kind: 'lastDays', days: 7 });
const workouts = await terra.activity.workouts({ kind: 'lastDays', days: 1 });
```

{% endcode %}

These reads need SDK 2.2.1 or later:

| Read                                                                                                                                         | Returns                                                                                                                               | Platforms                                      |
| -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| [`daily.totals`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/react-native-sdk-2#daily.totals)                   | The store's own total for each local day: distance, active and basal energy, flights climbed, and exercise minutes on iOS             | Both                                           |
| [`daily.intraday`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/react-native-sdk-2#daily.intraday)               | The energy, distance and floors records those totals are made of                                                                      | Both. Basal energy iOS only                    |
| [`body.restingHeartRate`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/react-native-sdk-2#body.restingheartrate) | The store's resting heart rate                                                                                                        | Both. Needs `daily` on Android                 |
| [`body.readings`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/react-native-sdk-2#body.readings)                 | Weight, height, body fat, lean mass, HRV, VO2 max, blood pressure, blood glucose, oxygen saturation, temperature and respiratory rate | Both. BMI and wrist temperature iOS only       |
| [`activity.routes`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/react-native-sdk-2#activity.routes)             | Each workout's GPS track                                                                                                              | Both                                           |
| [`activity.intensity`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/react-native-sdk-2#activity.intensity)       | Speed, power and cycling cadence                                                                                                      | Both. No `workoutId` on Android                |
| [`nutrition.quantities`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/react-native-sdk-2#nutrition.quantities)   | One value per nutrient, not grouped into meals                                                                                        | Both                                           |
| [`nutrition.hydration`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/react-native-sdk-2#nutrition.hydration)     | Water, one sample per drink, in millilitres                                                                                           | Both                                           |
| [`menstruation.flow`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/react-native-sdk-2#menstruation.flow)         | One flow value per logged day                                                                                                         | Both                                           |
| [`menstruation.events`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/react-native-sdk-2#menstruation.events)     | Ovulation tests, cervical mucus and intermenstrual bleeding                                                                           | Both                                           |
| [`athlete?.profile()`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/react-native-sdk-2#athlete.profile)          | Biological sex, date of birth, wheelchair use and move mode                                                                           | iOS. `terra.athlete` is `undefined` on Android |

Request the category before you read it. A read of a category your app never requested rejects with `authorization_not_determined`. In 2.2.1 and later, a `Workout` also carries `title`, and [`terra.availability()`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/react-native-sdk-2#availability) answers whether the device can serve `'health'` at all, before `connect`.

{% code lineNumbers="true" %}

```typescript
const availability = await terra.availability();
if (availability.kind !== 'available') return;

await terra.permissions.request({
  read: terra.athlete ? ['daily', 'nutrition', 'menstruation', 'athlete'] : ['daily', 'nutrition', 'menstruation'],
});
const totals = await terra.daily.totals({ kind: 'lastDays', days: 7 });
const quantities = await terra.nutrition.quantities({ kind: 'lastDays', days: 7 });
const flow = await terra.menstruation.flow({ kind: 'lastDays', days: 90 });
const profile = await terra.athlete?.profile();
```

{% endcode %}

To write data your app measured itself, use `log` on the `activity`, `body`, `daily`, `nutrition` and `sleep` namespaces, and [`delete`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/react-native-sdk-2#delete) with the ID it returns.

## Disconnect a user

{% code lineNumbers="true" %}

```typescript
await terra.disconnect();
```

{% endcode %}

`disconnect` forgets the link on the device and tells Terra to stop accepting its key. To remove the user from Terra, call [deauthenticateUser](https://docs.tryterra.co/reference/health-and-fitness-api/readme#delete-auth-deauthenticateuser) from your backend.

## Handle errors

Every rejection is a `TerraError` with a stable `code`, a `retryable` flag, and a `details` object:

{% code lineNumbers="true" %}

```typescript
import { EndUser, TerraError } from '@tryterra/react-native';

try {
  await terra.connect('health', EndUser.reference(user.id));
} catch (error) {
  if (error instanceof TerraError && error.code === 'token_rejected') {
    // mint a fresh token and try again
  }
}
```

{% endcode %}

The [React Native SDK reference](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/react-native-sdk-2) lists every code and which platform raises it.

## Troubleshooting

| Symptom                                                                                  | Cause                                                                                                                                             | Fix                                                                                                                                                     |
| ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Every call rejects with `bridge.not_bootstrapped`                                        | iOS: `TerraReactNative.bootstrap()` is missing or `TerraDevID` is not in `Info.plist`. Android: the `co.tryterra.sdk.DEV_ID` meta-data is missing | Add the line named in `details.line`                                                                                                                    |
| `connect` rejects with `bridge.token_timeout` (iOS) or `token_provider_failed` (Android) | `tokenProvider` did not answer within `tokenTimeoutMs`, or it rejected                                                                            | Check your token endpoint. Raise `tokenTimeoutMs` if it is slow                                                                                         |
| `connect` rejects with `token_rejected`                                                  | The token expired (three minutes) or was already used                                                                                             | Mint a fresh token for every link                                                                                                                       |
| `permissions.request` rejects with `bridge.no_activity` (Android)                        | No activity was on screen to present the sheet                                                                                                    | Request permissions while your app is in the foreground                                                                                                 |
| The Android sheet closes without asking                                                  | The request reads a type the manifest does not declare                                                                                            | Declare every Health Connect type the request reads                                                                                                     |
| `diagnostics.state()` is `{ state: 'linkRevoked' }`                                      | Terra stopped accepting this device's key                                                                                                         | Call `connect` again with the same end user                                                                                                             |
| `background.isRegistered()` is `false`                                                   | The native bootstrap did not run at launch                                                                                                        | iOS: call `bootstrap()` in `didFinishLaunchingWithOptions`. Android: keep WorkManager's default initializer, or call `TerraReactNative.bootstrap(this)` |

{% hint style="info" %}
**The mobile SDKs are only for Apple Health, Health Connect and Samsung Health.** These have no web API, so your app reads them on the phone and sends the data to Terra. Every other provider connects through the [Unified API](https://docs.tryterra.co/unified-api/getting-started) with no SDK.
{% endhint %}

## Next steps

* [Planned workouts on Apple Watch](/mobile-sdks/planned-workouts-apple-watch.md): put plans on the user's Watch from iOS.
* [Health data events](/unified-api/receiving-data-updates.md): handle the data the SDK uploads.
* [Historical data requests](/unified-api/requesting-historical-data.md): backfill beyond the first sync.
