> 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/ios-sdk-2.md).

# iOS SDK

Install the TerraiOS SDK, connect Apple Health and keep its data flowing to Terra in the background.

This page shows how to connect Apple Health with `TerraiOS` 2.x. The SDK reads Apple Health, uploads it to Terra in the background, and delivers it to your [data destination](/destinations/setting-up-data-destinations.md) like any other provider.

The Swift module is `TerraSDK` and its entry point is `Terra`. Code written against 1.x does not compile against 2.0.0. If you are upgrading, read [iOS upgrade guide](/mobile-sdks/ios-migrating-to-2.md).

## Before you begin

* iOS 15 or later.
* A backend endpoint that mints Terra auth tokens.

## Install the SDK

{% tabs %}
{% tab title="Swift Package Manager" %}
In Xcode, add the package `https://github.com/tryterra/TerraiOS` with the rule **Up to Next Major Version** from `2.1.0`, or in `Package.swift`:

{% code title="Package.swift" %}

```swift
.package(url: "https://github.com/tryterra/TerraiOS", from: "2.1.0")
```

{% endcode %}

Link the `TerraSDK` product into your app target.
{% endtab %}

{% tab title="CocoaPods" %}
{% code title="Podfile" %}

```ruby
target 'YourApp' do
  pod 'TerraiOS', '~> 2.0'
end
```

{% endcode %}
{% endtab %}
{% endtabs %}

### Add capabilities and Info.plist keys

Add these in **Signing & Capabilities** and in your app's `Info.plist`.

| Where        | Key                                                 | Value                                                                                                                    |
| ------------ | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Entitlements | `com.apple.developer.healthkit`                     | `true`                                                                                                                   |
| Entitlements | `com.apple.developer.healthkit.access`              | empty array                                                                                                              |
| Entitlements | `com.apple.developer.healthkit.background-delivery` | `true`                                                                                                                   |
| Info.plist   | `NSHealthShareUsageDescription`                     | Why your app reads health data                                                                                           |
| Info.plist   | `NSHealthUpdateUsageDescription`                    | Why your app writes health data. Required even if your app never writes: App Store Connect rejects the upload without it |
| Info.plist   | `UIBackgroundModes`                                 | `fetch`                                                                                                                  |
| Info.plist   | `BGTaskSchedulerPermittedIdentifiers`               | `co.tryterra.sdk.refresh`                                                                                                |

The `TerraSDK.framework` carries its own privacy manifest (`PrivacyInfo.xcprivacy`), so Xcode's privacy report includes it.

## Mint auth tokens on your backend

The SDK links a device with a single-use auth token. Mint it on your backend with your API key, and pass your own user identifier as `reference_id`:

{% 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 %}

Tokens expire after three minutes and are spent on first use. Expose an endpoint in your own API that mints one for the signed-in user and returns it to the app.

## Connect Apple Health

{% code title="YourApp.swift" %}

```swift
import TerraSDK

let terra = Terra(devID: "<YOUR_DEV_ID>") {
    try await yourBackend.mintTerraToken()
}

// Once, at launch
BackgroundSync.register(terra: terra)

// When the user taps "Connect Apple Health".
// user.id is your ID for the signed-in person: the same value your backend sends as reference_id.
try await terra.connect(.health, for: .reference(user.id))
try await terra.permissions.request(read: [.daily, .sleep, .activity, .body])
```

{% endcode %}

That is the whole integration. After the permission sheet is answered, the SDK uploads on its own.

### Token provider

`connect` does not take a token. The closure you pass to [`Terra(devID:tokenProvider:)`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#terra.init) supplies one, and the SDK calls it only when a new link is needed:

* the first time the device links,
* after a reinstall or a restored backup that lost the stored link,
* when a different person connects than the one the stored link was made for,
* when Terra no longer accepts the device's key and the SDK has to link again.

A normal launch for the same person restores the link from the Keychain with no network call and never calls the closure. The closure runs off the main actor and outlives the call that built it, so capture what it needs rather than a view.

If the token arrives some other way, such as a scanned QR code, pass it directly with [`connect(_:for:using:)`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#terra.connect). A token passed this way always creates a fresh link.

### Name the end user

Every `connect` names the person it is for: [`.reference(id)`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#enduser) with the same value your backend sends as `reference_id`, or `.anonymous` if your backend sends none. A stored link is restored only for the person it was made for:

* **The same person:** the link is restored with no network call.
* **Someone else signed in on this phone:** the SDK retires the old link the way [`disconnect()`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#connection.disconnect) does, then links again with a token from the provider.
* **A link made before connections recorded their end user:** it is kept and recorded as this person's, with no network call.

This stops a second account on a shared phone from receiving the first account's link, and its data from arriving under the first account's `reference_id`. The SDK stores a salted hash of the reference, never the reference itself. The end user decides only whether the stored link may be restored: Terra still trusts only the reference inside the token, so mint the token for the same person you pass to `connect`. `disconnect()` clears the recorded end user with the link.

### Build `Terra` once, at launch

Build one `Terra` per app, at launch, and keep it. Register background sync in the same place, before the first `await`. iOS raises an exception, and the app stops, when a background task is registered after the app finishes launching.

{% code title="YourApp.swift" %}

```swift
import SwiftUI
import TerraSDK

@MainActor
final class HealthModel: ObservableObject {
    let terra = Terra(devID: "<YOUR_DEV_ID>") {
        try await YourBackend.shared.mintTerraToken()
    }

    init() {
        BackgroundSync.register(terra: terra)
    }

    func connect(userID: String) async throws {
        let connection = try await terra.connect(.health, for: .reference(userID))
        print("Terra user_id:", connection.userID)
        try await terra.permissions.request(read: [.daily, .sleep, .activity, .body])
    }
}

@main
struct YourApp: App {
    @StateObject private var model = HealthModel()

    var body: some Scene {
        WindowGroup { ContentView(model: model) }
    }
}
```

{% endcode %}

[`connection.userID`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#connection) is the Terra `user_id` that appears on your webhooks. [`terra.connections`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#terra.connections) lists what is linked on this device.

## Request permissions

Request reads and writes in one call:

{% code title="YourApp.swift" %}

```swift
try await terra.permissions.request(
    read: [.activity, .body, .daily, .sleep],
    write: [.activity, .body]
)
```

{% endcode %}

`write: [.activity]` asks for workouts and everything a workout can carry: active energy, and walking and running, cycling and swimming distance. If your app logs only some workouts, ask for those instead with [`WorkoutScope`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#workoutscope), and leave `.activity` out of `write`. The sheet then shows only those rows:

{% code title="YourApp.swift" %}

```swift
try await terra.permissions.request(
    read: [.activity, .body, .daily, .sleep],
    write: [.body],
    workouts: [WorkoutScope(activity: .running, energy: true, distance: true)]
)
```

{% endcode %}

The categories are `daily`, `body`, `sleep`, `activity`, `nutrition`, `menstruation`, `athlete` and [`healthObservation`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#datacategory).

To read the current state:

{% code title="YourApp.swift" %}

```swift
await terra.permissions.writeStatus(for: .body)   // granted, denied or notDetermined
await terra.permissions.status(for: .body)        // may be .notDisclosed
await terra.permissions.requested                 // everything asked for so far
```

{% endcode %}

[`status(for:)`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#permissions.status) answers for reads. iOS never reveals whether a read was granted or denied, so on iOS it never returns `.granted` or `.denied`:

| Status          | Meaning                                                                                                    |
| --------------- | ---------------------------------------------------------------------------------------------------------- |
| `.notDisclosed` | Your app asked, and iOS will not say what the person chose. A denied read and an empty store look the same |
| `.unavailable`  | This device has no Apple Health, such as an iPad on iPadOS 16 or earlier. Hide the feature                 |
| `.unsupported`  | The device has Apple Health, but the SDK cannot read this category yet. Hide this category only            |

[`writeStatus(for:)`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#permissions.writestatus) answers for writes, which iOS does disclose: `.granted`, `.denied` or `.notDetermined`. For `.activity` it is `.granted` only when every type you asked to write there is granted. A workout `log` checks only the types its entries carry, so a run can still log while `writeStatus(for: .activity)` is `.denied`, for example because the person refused swimming distance.

To upload heart rate, request `body`: the heart rate and resting heart rate permissions come with it. Heart rate is uploaded on the daily route, and asking for `body` also starts the daily upload.

## Set up background delivery

[`BackgroundSync.register`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#backgroundsync.register) is the only call needed for uploads to continue while the app is closed. Call it once at launch, before the first `await`.

{% code title="YourApp.swift" %}

```swift
BackgroundSync.register(terra: terra)
```

{% endcode %}

| Parameter          | Type           | Default   | Description                                                                                                                                      |
| ------------------ | -------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `terra`            | `Terra`        | required  | The client to run on each wake. Build it at launch, not inside a view                                                                            |
| `source`           | `Source`       | `.health` | The source whose HealthKit observation is restored on launch                                                                                     |
| `earliestInterval` | `TimeInterval` | `3600`    | The soonest iOS should consider waking the app again, in seconds. A floor, not a schedule: iOS decides when based on how the person uses the app |

It returns `true` when this call registered the task with iOS. `false` means one of two things:

* `co.tryterra.sdk.refresh` is missing from `BGTaskSchedulerPermittedIdentifiers`. The app is never woken for it.
* The task is already registered, for example from a second launch path. This is harmless.

The SDK also needs the `com.apple.developer.healthkit.background-delivery` entitlement. Without it HealthKit does not wake the app when new data arrives, and uploads only happen when iOS grants a background refresh slot. The SDK keeps working and reports `backgroundObservationUnavailable` on [`diagnostics.events()`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#diagnostics.events).

Background delivery only runs while the phone is unlocked and has a network connection. HealthKit data is encrypted while the phone is locked.

### Run Terra from your own background task

iOS keeps one pending background refresh request per app. If your app schedules its own refresh task and also calls `register`, one of the two never runs, and the SDK reports `backgroundRefreshUnscheduled` on [`diagnostics.state()`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#diagnostics.state). Keep your task and run Terra's pass from it.

* [`registerForHostTask`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#backgroundsync.registerforhosttask): does everything `register` does at launch except ask iOS for a task. It restores the HealthKit observers a background launch needs, and cancels any refresh request the SDK left pending, so the slot is free for yours.
* [`run`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#backgroundsync.run): the same pass the SDK's own task runs. Pass its result to `setTaskCompleted(success:)`.

**Register for your task at launch.** Call `registerForHostTask` at launch, before the first `await`, instead of `BackgroundSync.register(terra:)`.

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

```swift
BackgroundSync.registerForHostTask(terra: terra)
```

{% endcode %}

**Run Terra's pass from your handler.**

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

```swift
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.example.refresh", using: .main) { task in
    scheduleNextRefresh()   // your app's own resubmit
    let work = Task { await BackgroundSync.run(terra: terra) }
    task.expirationHandler = { work.cancel() }
    Task { task.setTaskCompleted(success: await work.value) }
}
```

{% endcode %}

{% hint style="warning" %}
Register the handler with `using: .main`. A handler written in a SwiftUI `App`, or any other `@MainActor` code, is isolated to the main actor, and iOS calling it on a background queue stops the app the first time the task runs.
{% endhint %}

{% hint style="info" %}

* Cancelling the task stops the pass from starting new work. Nothing it already sent is sent again.
* `run` has 18 seconds by default. Pass `within:` with less when your own work shares the task, or more from a `BGProcessingTask`.
* `co.tryterra.sdk.refresh` can stay in `BGTaskSchedulerPermittedIdentifiers` or be removed.
* Without any background task, HealthKit wakes still upload what changed. A first sync's history and queued history requests then wait for the next foreground run or wake.
  {% endhint %}

## What syncs and when

The SDK decides when to upload:

| Trigger          | When                                                                                                                                                                                                 |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Permission grant | Straight after a [`permissions.request`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#permissions.request) that first asks for a category with an upload route |
| Foreground       | Each time the app becomes active, at most once every 15 minutes                                                                                                                                      |
| Background       | When HealthKit reports new data, and on the scheduled background refresh task                                                                                                                        |
| Relink           | After `connect` creates a new link, if the app had already requested access                                                                                                                          |

### How far back

The first sync reaches two years (730 days) back.

After every sync, we upload what has changed since the previous sync. If you log a workout today with last Tuesday's date, Terra still syncs that data.

To backfill further, [request historical data](/unified-api/requesting-historical-data.md) for the user from your backend with `to_webhook=true`. Terra queues the window for the device, and the next run uploads it.

### What is uploaded

| Category            | What is uploaded                                                                                                                                                                                                                    |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `daily`             | Steps, distances, energy, floors, exercise, move and stand time, and heart rate when `body` is granted, as whole local days                                                                                                         |
| `sleep`             | Sleep stages, as whole wake dates                                                                                                                                                                                                   |
| `activity`          | Workouts, including stats, events and routes                                                                                                                                                                                        |
| `body`              | Weight, height, body fat, lean mass, BMI, waist circumference, blood pressure, blood glucose, oxygen saturation, body and wrist temperature, HRV, respiratory rate, VO2 max and insulin delivery. Heart rate rides the daily upload |
| `nutrition`         | Meals and their nutrients, and water                                                                                                                                                                                                |
| `menstruation`      | Menstrual flow, and cycle events such as spotting, ovulation tests and pregnancy tests                                                                                                                                              |
| `athlete`           | Biological sex, date of birth, wheelchair use and move mode, as one snapshot                                                                                                                                                        |
| `healthObservation` | Symptoms, toothbrushing and handwashing, inhaler use, falls, blood alcohol and UV exposure                                                                                                                                          |

A run only uploads the categories your app requested.

## Read and write on the device

Local reads go straight to HealthKit and never touch the network:

{% code title="YourApp.swift" %}

```swift
let heartRate = try await terra.body.heartRate(in: .lastDays(7))
let steps = try await terra.daily.steps(in: .lastDays(7))
let sleep = try await terra.sleep.segments(in: .lastDays(1))
let workouts = try await terra.activity.workouts(in: .lastHours(24))
```

{% endcode %}

These reads need SDK 2.2.0 or later. Each takes a `TimeRange` except `athlete.profile()`. Request each read's category first:

| Read                                                                                                                                            | Returns                                                                                                                                    | Category       |
| ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------- |
| [`daily.totals(in:)`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#dailymetrics.totals)                   | HealthKit's own total for each local day: distances, active and basal energy, flights climbed and exercise minutes                         | `daily`        |
| [`daily.intraday(in:)`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#dailymetrics.intraday)               | The energy, distance and floors samples those totals are made of                                                                           | `daily`        |
| [`body.restingHeartRate(in:)`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#bodymetrics.restingheartrate) | HealthKit's resting heart rate, one per day                                                                                                | `body`         |
| [`body.readings(in:)`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#bodymetrics.readings)                 | Weight, height, BMI, body fat, lean mass, HRV, VO2 max, blood pressure, blood glucose, oxygen saturation, temperature and respiratory rate | `body`         |
| [`activity.routes(in:)`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#activitymetrics.routes)             | Each workout's GPS track                                                                                                                   | `activity`     |
| [`activity.intensity(in:)`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#activitymetrics.intensity)       | Speed, power and cycling cadence recorded inside workouts                                                                                  | `activity`     |
| [`nutrition.quantities(in:)`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#nutritionmetrics.quantities)   | One value per nutrient sample, not grouped into meals                                                                                      | `nutrition`    |
| [`nutrition.hydration(in:)`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#nutritionmetrics.hydration)     | Water, one sample per drink                                                                                                                | `nutrition`    |
| [`menstruation.flow(in:)`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#menstruationmetrics.flow)         | One flow value per logged day                                                                                                              | `menstruation` |
| [`menstruation.events(in:)`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#menstruationmetrics.events)     | Ovulation tests, cervical mucus and intermenstrual bleeding                                                                                | `menstruation` |
| [`athlete.profile()`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#athletemetrics.profile)                | Biological sex, date of birth, wheelchair use and move mode                                                                                | `athlete`      |

In 2.2.0 and later, a [`Workout`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#workout) also carries `title`, the name the writing app gave it, and [`terra.availability()`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#terra.availability) answers whether the device has HealthKit at all. It needs no connection, so you can call it before `connect`.

{% code title="YourApp.swift" %}

```swift
guard await terra.availability() == .available else { return }

let totals = try await terra.daily.totals(in: .lastDays(7))
let readings = try await terra.body.readings(in: .lastDays(30))
let quantities = try await terra.nutrition.quantities(in: .today)
let meals = Dictionary(grouping: quantities, by: \.foodCorrelationID)
let profile = try await terra.athlete.profile()
```

{% endcode %}

To write data your app measured itself:

{% code title="YourApp.swift" %}

```swift
let ids = try await terra.activity.log([workoutEntry])
try await terra.activity.delete(id: ids[0])
```

{% endcode %}

The write namespaces are `activity`, `body`, `daily`, `nutrition` and `sleep`. [`log`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#activitymetrics.log) returns one `UUID` per entry, which is the only handle for deleting it later. HealthKit only lets you delete samples your app wrote.

Deleting a workout also deletes the active energy and distance logged with it, including on workouts an earlier SDK release logged. An energy or distance type the person stopped your app writing after the workout was logged stays in Apple Health.

## Disconnect a user

{% code title="YourApp.swift" %}

```swift
for connection in try await terra.connections {
    try await connection.disconnect()
}
```

{% endcode %}

[`disconnect()`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#connection.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.

## Troubleshooting

Start with the SDK's own diagnostics:

{% code title="YourApp.swift" %}

```swift
await terra.diagnostics.state()         // healthy, quiet(since:), backgroundRestricted, pendingRetry, ...
await terra.diagnostics.lastDelivery()  // when data last reached Terra
for await event in terra.diagnostics.events() { /* typed events */ }
```

{% endcode %}

When the SDK gives up on something, such as a delivery out of retries, it sends a redacted report to Terra so support can see the device's side. The report never contains a health value.

| Symptom                                                                                                                                         | Cause                                                                                                                                 | Fix                                                                                                                                                                           |
| ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `connect` throws [`tokenRejected`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#terraerror)               | The token expired (three minutes) or was already used                                                                                 | Mint a fresh token for every link. Check the delay between minting and connecting                                                                                             |
| `connect` throws [`devIDMismatch`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#terraerror)               | A different dev ID already linked on this device. A `connect` with a token is refused too                                             | Run with the original dev ID, or erase the Keychain: the record survives reinstalling the app. On a simulator, `xcrun simctl keychain <device> reset`. Use one dev ID per app |
| [`diagnostics.state()`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/ios-sdk-2#diagnostics.state) is `.linkRevoked` | Terra stopped accepting this device's key, for example because another device linked the same user                                    | Call `connect(.health, for:)` again with the same end user. The token provider is called to link again                                                                        |
| `diagnostics.state()` is `.backgroundRefreshUnscheduled`                                                                                        | Your app submitted its own `BGAppRefreshTaskRequest` first. iOS keeps one pending refresh request per app and drops the second        | Run Terra from your own task with [`registerForHostTask`](#run-terra-from-your-own-background-task)                                                                           |
| No background uploads at all                                                                                                                    | `co.tryterra.sdk.refresh` missing from `BGTaskSchedulerPermittedIdentifiers`, Background fetch off, or `register` called after launch | Fix the Info.plist entry and call `register` before the first `await`. Check its return value                                                                                 |
| Uploads only when the app opens                                                                                                                 | Missing `healthkit.background-delivery` entitlement                                                                                   | Add the entitlement. Look for `backgroundObservationUnavailable` on `diagnostics.events()`                                                                                    |
| `diagnostics.state()` is `.quiet(since:)`                                                                                                       | No new data since that date                                                                                                           | Often a person who has not worn their watch. iOS does not disclose denied reads, so this is a hint, not an error                                                              |
| A category is missing in Terra                                                                                                                  | The app never requested it, or the person did not grant it                                                                            | Request it in `permissions.request`. iOS does not disclose denied reads, so ask the person to check **Settings > Health > Data Access & Devices**                             |

### App extensions

An app extension can read and write on grants inherited from the host app, but it cannot present a permission sheet and never syncs on its own. To share state between the host and an extension, use an App Group and a Keychain access group.

{% 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.
* [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.
