# HealthKit and Health Connect

Canonical page: https://docs.ovok.com/native-sdk/guide/modules/data-sync

Use the data sync module to import selected records from Apple HealthKit on iOS or
Health Connect on Android. The authorization provider handles the platform permission
flow; its matching sync component reads records and saves them as OVOK observations
for the signed-in patient.

Apple Health import needs `@kingstinct/react-native-healthkit` 16.2.0 or newer and
`react-native-nitro-modules` 0.35 or newer. Both are native modules: after you add or
upgrade them, build the app again, because a JavaScript update cannot load them.
HealthKit identifiers are plain strings such as `"HKQuantityTypeIdentifierBodyMass"`;
version 16 has no `HKQuantityTypeIdentifier` enum to import.

Before adding the integration, complete [installation and native setup](/native-sdk/guide/how-to/installation)
and mount the app inside the authenticated [OVOK provider tree](/native-sdk/guide/how-to/app-shell). Health
sync needs an initialized client and a patient profile, and it runs in a native
development build rather than Expo Go.

## Pick the platform API [#pick-the-platform-api]

| Platform | Authorization provider | Import component | Native source |
| --- | --- | --- | --- |
| iOS | `DataSync.AppleHealthAuthorizationProvider` | `DataSync.AppleHealthSync` | HealthKit |
| Android | `DataSync.AndroidHealthConnectAuthorizationProvider` | `DataSync.AndroidHealthSync` | Health Connect |

Import the module from `@ovok/native/data-sync`. The optional, adapter-driven
[`HealthImportCard`](/native-sdk/guide/components/ui/health-import-card) is a separate UI primitive;
it does not replace these providers or run this import pipeline.

## 1. Map records to OVOK observations [#1-map-records-to-ovok-observations]

Each `dataToSync` entry connects an OVOK measurement key to one or more platform
record identifiers. Leave `code` out: the SDK codes a core measurement key the way
core codes that measurement from any source, and any other key by its reviewed
LOINC term or the platform's own type. Set `code` and `system` only to store a
type under a different code.

~~~ts
import { MeasurementTypeKey } from "@ovok/core/next";
import type { AppleHealthSyncProps } from "@ovok/native/data-sync";

export const appleDataToSync: AppleHealthSyncProps["dataToSync"] = {
  [MeasurementTypeKey.bodyWeight]: {
    typeIdentifiers: [
      { typeIdentifier: "HKQuantityTypeIdentifierBodyMass" },
    ],
    minDate: new Date("2025-01-01T00:00:00.000Z"),
  },
};
~~~

Android uses Health Connect record names for the same mapping:

~~~ts
import { MeasurementTypeKey } from "@ovok/core/next";
import type { AndroidHealthSyncProps } from "@ovok/native/data-sync";

export const androidDataToSync: AndroidHealthSyncProps["dataToSync"] = {
  [MeasurementTypeKey.bodyWeight]: {
    typeIdentifiers: [
      { typeIdentifier: "Weight" },
    ],
    minDate: new Date("2025-01-01T00:00:00.000Z"),
  },
};
~~~

### Apple Watch ECG [#apple-watch-ecg]

Map `MeasurementTypeKey.ecg` to the `HKElectrocardiogramType` identifier to import
the recordings made with the Apple Watch ECG app (iOS 14 or later):

~~~ts
[MeasurementTypeKey.ecg]: {
  typeIdentifiers: [{ typeIdentifier: "HKElectrocardiogramType" }],
},
~~~

Each recording becomes OVOK's ECG observation: the waveform in millivolts as sampled
data, its duration in seconds, and the average heart rate as a second observation. The
start of the recording is its time. Apple's own classification (such as sinus rhythm
or atrial fibrillation) and the symptoms the person noted are not imported: the SDK
saves what was measured and gives no reading of it. Add `HKElectrocardiogramType` to
`readIdentifiers` of the authorization provider as well. Recordings are read at most
ten at a time, whatever `chunkSize` is, because each one holds the whole waveform.

The first read defaults to the most recent 30 days on both platforms. Set
`minDate` to choose an earlier initial boundary. Later runs use the latest matching
observation already stored for the patient and provider as their starting point.
Daily totals and summaries re-read the last seven imported days, so a total that a
wearable changes after a late sync is updated. The SDK also keeps provider pagination
anchors in the native client's secure storage, scoped to the current tenant,
project, patient, provider, and data type.

When a record is deleted in Apple Health or Health Connect, the observations imported
from it are deleted too. They are found by the `observation-sync-record` identifier
the import gives them; observations imported before it existed don't have it, so
they stay.

The SDK has built-in modes for common data shapes:

| `mode` | Result |
| --- | --- |
| `point` | One observation per source sample or record. This is the default for most measurement keys. |
| `daily-total` | One total for each completed day. This is the default for step count, distance, active energy, flights climbed, exercise minutes, water, carbohydrates, and calories eaten. |
| `daily-summary` | One daily summary. Heart rate defaults to an average with minimum and maximum components. |
| `session` | One observation for each session. Sleep and workout default to this mode. |
| `series` | One observation per finished hour holding a value a minute as `valueSampledData` (`E` for a minute without one), for frequent quantities such as heart rate, SpO₂ and respiratory rate. Apple Health's statistics and Health Connect's aggregation give the minute averages, following the user's source priority; Health Connect types it doesn't aggregate (SpO₂, respiratory rate) are averaged from their records. Each run reads the last three imported hours again and writes over them. |

The exported `HealthDataType` enum includes `height`, `bodyFatPercentage`,
`bodyMassIndex`, `leanBodyMass`, `distance`, `activeEnergy`,
`flightsClimbed`, `exerciseMinutes`, `heartRateSummary`, `sleep`, `workout`,
`water`, `carbohydrates`, `caloriesEaten`, `bloodAlcohol`, and `insulin`.
Health Connect has no blood alcohol or insulin record. Use these keys for data that is not represented by a core measurement
key. Set `mode` on an entry to override its default.

## 2. Configure native permissions [#2-configure-native-permissions]

Add the SDK config plugin for the platform integrations your app uses. For an
import-only flow, request only read permissions for the record types in
`dataToSync`:

~~~ts
plugins: [
  [
    "@ovok/native",
    {
      healthKit: true,
      healthConnect: true,
    },
  ],
],
ios: {
  infoPlist: {
    NSHealthShareUsageDescription:
      "Explain why the app reads the selected health data.",
  },
},
android: {
  permissions: ["android.permission.health.READ_WEIGHT"],
},
~~~

For HealthKit background delivery, use `healthKit: { background: true }` and add
the background delivery option in step 5. Add
`NSHealthUpdateUsageDescription` and HealthKit write permissions only if your app
also writes data back to the health store. For Health Connect, declare the matching
`android.permission.health.READ_*` permissions for every record type you request.
The plugin composes the platform libraries; native config changes take effect after
you rebuild the app.

See [installation and native setup](/native-sdk/guide/how-to/installation) for plugin details, usage
descriptions, and development builds.

## 3. Mount the authorization provider and sync component [#3-mount-the-authorization-provider-and-sync-component]

The provider requests access automatically when a request is needed. Pass only the
identifiers the feature will read. For import-only flows, omit `writeIdentifiers`.
The mapping objects, status setters, progress state, and error handler below are
app-owned values; define them in the screen or feature module.

~~~tsx
import * as React from "react";
import { ActivityIndicator } from "react-native";
import { DataSync } from "@ovok/native/data-sync";

export function AppleHealthImport() {
  return (
    <DataSync.AppleHealthAuthorizationProvider
      readIdentifiers={["HKQuantityTypeIdentifierBodyMass"]}
      fallback={<ActivityIndicator />}
      onAuthorizationStatusChange={setHealthKitStatus}
    >
      <DataSync.AppleHealthSync
        dataToSync={appleDataToSync}
        onProgress={setProgress}
        onError={handleSyncError}
      />
    </DataSync.AppleHealthAuthorizationProvider>
  );
}
~~~

For Health Connect, use its record identifiers and provider:

~~~tsx
import { ActivityIndicator } from "react-native";
import { DataSync } from "@ovok/native/data-sync";

export function HealthConnectImport() {
  return (
    <DataSync.AndroidHealthConnectAuthorizationProvider
      readIdentifiers={["Weight"]}
      fallback={<ActivityIndicator />}
      onAuthorizationStatusChange={setHealthConnectStatus}
    >
      <DataSync.AndroidHealthSync
        dataToSync={androidDataToSync}
        onProgress={setProgress}
        onError={handleSyncError}
      />
    </DataSync.AndroidHealthConnectAuthorizationProvider>
  );
}
~~~

Both sync components read the active native client and Patient session from
`@ovok/core/next/react`. Mount them only after sign-in. The provider
controls the permission UI; the sync component starts its import when mounted.

### Own the permission screen [#own-the-permission-screen]

Provide both `renderManualRequestUI` and `skipRequest` when your app owns the
prompt. Set `skipRequest={false}` to let the provider render the callback UI and
avoid automatic permission requests:

~~~tsx
import { Button } from "react-native";
import { DataSync } from "@ovok/native/data-sync";

<DataSync.AndroidHealthConnectAuthorizationProvider
  readIdentifiers={["Weight"]}
  skipRequest={false}
  renderManualRequestUI={(requestAccess) => (
    <Button title="Continue" onPress={requestAccess} />
  )}
>
  <DataSync.AndroidHealthSync dataToSync={androidDataToSync} />
</DataSync.AndroidHealthConnectAuthorizationProvider>
~~~

The callback receives the provider's `requestAccess` function. To build a bottom
sheet with the supplied pieces, see
[`ManuallyRequestSheet`](/native-sdk/guide/components/data-sync/manually-request-sheet/). A Skip
action is app behavior: update the screen or navigation and do not leave an import
component mounted behind a skipped permission prompt. Setting `skipRequest={true}`
bypasses the provider's request gate and renders its children.

Apple does not expose whether a person denied read access to a particular HealthKit
type. Its status reports types that still need a response, but a `ready` state is
not proof that every requested type is readable. Treat empty reads as a valid
outcome.

## 4. Render progress and handle errors [#4-render-progress-and-handle-errors]

The sync components report progress as a partial map keyed by the configured
measurement key. Render it with the exported `SyncProgressList` or provide your own
UI:

~~~tsx
import { SyncProgressList } from "@ovok/native/data-sync";

<SyncProgressList
  progress={progress}
  labelForType={(type) => measurementLabels[type] ?? type}
  statusLabels={{ syncing: "Importing", completed: "Imported" }}
/>;
~~~

`SyncProgress` includes `measurementTypeKey`, `status`, optional pause
`reason`, and optional `progress.done` / `progress.total` counts. Status values
are `idle`, `started`, `syncing`, `paused`, `failed`, and `completed`.
Pause reasons are `offline` and `wifi-only`.

`chunkSize` defaults to **250 records** per provider read. `wifiOnly` defaults to
`false`; when the device is offline, the sync pauses, and when `wifiOnly` is true,
it also pauses on non-Wi-Fi networks. The sync resumes when the connection meets the
configured policy.

`onError` receives a `Bundle<Resource>`, an `Error`, or `null`. Narrow the
value before reading error-specific fields, and report failures through the app's
normal error handling.

Health saves retry HTTP 429, 500, 502, 503 and 504 responses up to three times. A refusal (400, 401, 403, 404,
409, 422) and a 501, a feature the environment does not have, fail at once. Each wait is the
greater of the normal 1, 4, and 16 second backoff and the server's `Retry-After`
hint. If all retries fail, the final error reaches `onError`.

## 5. Add background delivery only when needed [#5-add-background-delivery-only-when-needed]

For HealthKit observer updates, pass `backgroundDelivery` to
`AppleHealthSync` and enable the HealthKit background capability in the config
plugin. Background delivery asks iOS to notify the app about new samples; the OS
controls when the app runs.

For Android Health Connect imports after the app process stops, use the registered
headless task and scheduler in the [background sync guide](/native-sdk/guide/modules/background-sync).
Set `backgroundAccess` on the authorization provider to request Health Connect's
separate background-read permission. That permission is reported independently from
foreground access, and declining it does not block foreground imports.

Do not render `AndroidHealthSync` inside a headless task. The headless API runs an
import with an explicitly restored client, patient ID, and mapping.

## Component references [#component-references]

- [DataSync module and compound API](/native-sdk/guide/components/data-sync/)
- [Apple Health authorization provider](/native-sdk/guide/components/data-sync/apple/health-authorization-provider/)
- [Apple Health sync](/native-sdk/guide/components/data-sync/apple/health-sync/)
- [Android Health Connect authorization provider](/native-sdk/guide/components/data-sync/android/health-connect-authorization-provider/)
- [Android Health sync](/native-sdk/guide/components/data-sync/android/health-sync/)
- [SyncProgressList](/native-sdk/guide/components/data-sync/sync-progress-list/)
- [ManuallyRequestSheet and its components](/native-sdk/guide/components/data-sync/manually-request-sheet/)
- [HealthImportCard](/native-sdk/guide/components/ui/health-import-card)

For a separate scheduled Android import, continue with
[background sync](/native-sdk/guide/modules/background-sync).

## API reference [#api-reference]

Every export of `@ovok/native/data-sync`, with signatures, props and types, is in the [`data-sync` API reference](/native-sdk/reference/data-sync).
