GuidesModules

HealthKit and Health Connect

Configure permissions and import device health data with the OVOK SDK

  • Stable unknown
  • Release candidate since 1.16.1-rc.3
  • Development since 1.16.1-dev.2111+9796905

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 and mount the app inside the authenticated OVOK provider tree. 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

PlatformAuthorization providerImport componentNative source
iOSDataSync.AppleHealthAuthorizationProviderDataSync.AppleHealthSyncHealthKit
AndroidDataSync.AndroidHealthConnectAuthorizationProviderDataSync.AndroidHealthSyncHealth Connect

Import the module from @ovok/native/data-sync. The optional, adapter-driven HealthImportCard is a separate UI primitive; it does not replace these providers or run this import pipeline.

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.

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:

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

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

[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:

modeResult
pointOne observation per source sample or record. This is the default for most measurement keys.
daily-totalOne 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-summaryOne daily summary. Heart rate defaults to an average with minimum and maximum components.
sessionOne observation for each session. Sleep and workout default to this mode.
seriesOne 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

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:

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 for plugin details, usage descriptions, and development builds.

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.

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:

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

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:

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. 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

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:

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

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. 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

For a separate scheduled Android import, continue with background sync.

API reference

Every export of @ovok/native/data-sync, with signatures, props and types, is in the data-sync API reference.

Last updated on

On this page