Skip to content

Guide · 10 min read

Apple HealthKit vs Google Health Connect for developers

Neither has a cloud API. How the two on-device stores differ in permissions, background delivery, history and what an aggregator can and cannot do for you.

Published · By the WearLink engineering team

Diagram of on-device health stores feeding a mobile app and then a backend, contrasted with cloud OAuth providers

If you searched for "Apple Health API" or "Health Connect API" hoping for a server endpoint, here is the fact that shapes everything else: neither Apple HealthKit nor Google Health Connect has a cloud API. Apple does not run a server that returns a user's heart rate. Google retired the one that did (Google Fit) and replaced it with an on-device store. Both are databases that live on the phone, readable only by an app the user installed and explicitly granted permission to.

Any vendor that implies server-side access to Apple Health, with or without an Apple logo on their homepage, is describing something impossible. That includes WearLink. What differs between vendors is what they do with the device side, and how well they handle the problems that follow from the data living on the phone: permissions that hide denial, intermittent connectivity, backfill cost, and duplicates from every app that writes to the store.

This guide compares the two stores at the level an experienced mobile engineer needs before committing to an architecture, then explains what an aggregator can and cannot honestly do for you.

Side by side

The two stores solve the same problem with different design choices. The table describes behaviour generically; exact API surfaces change with OS releases and the current platform documentation is the source of truth for specifics.

AxisApple HealthKitGoogle Health Connect
PlatformiOS and watchOSAndroid
Where the data livesOn the device, in the HealthKit store. No Apple server you can call.On the device, in the Health Connect store. No Google server you can call.
Server-side readImpossible by design. Only an installed app with permission can read.Impossible by design. The Google Fit REST API that once allowed it is deprecated.
Permission granularityPer data type, read and write requested separately.Per record type, read and write requested separately.
Is denial visible to the app?Not for reads. A denied type returns nothing, the same as an empty type.Yes, granted permissions can be queried. Empty data still returns nothing.
Background deliveryObserver queries with background delivery wake the app when new samples land. Frequency is throttled by the system and not guaranteed.No equivalent push. The app reads periodically, typically from scheduled background work, and uses change tokens to fetch only what is new.
Historical depthWhatever is in the store, often years, subject to the user’s own device and iCloud settings.Limited. Treat the on-device window as short and back it up on your side.
Aggregates third-party appsYes. Oura, Strava, Garmin and others write into it, each record tagged with its source.Yes. Garmin Connect, Samsung Health, Strava and others write into it, each record tagged with its data origin.
Permission UIA system sheet listing every requested type, shown on first request.A system permission screen owned by Health Connect, plus a rationale screen you must provide.
AvailabilityBuilt into the OS on every iPhone.A separate component whose presence and version vary by Android version and device. Needs a graceful absent path.

Two rows deserve emphasis. The background-delivery row decides your sync architecture: iOS can wake you when data changes, Android expects you to come back and ask. The historical depth row decides your storage architecture: if you need history beyond what the on-device window holds, your backend has to be the long-term store, and you have to pull data off the device before the window rolls.

Permissions: denied versus empty

Both stores ask the user per data type, so a user can grant steps and refuse heart rate. Where they differ is whether your app is told about the refusal.

HealthKit

HealthKit deliberately does not reveal read denials. The reasoning is privacy: if an app could see that a user refused to share a particular type, that refusal itself leaks something. The consequence for you is that a query against a denied type returns an empty result, indistinguishable from a type the user simply has no data for. You can find out whether you have asked for a type, which lets you avoid re-prompting, but you cannot find out the answer.

Health Connect

Health Connect has its own permission UI, separate from the ordinary Android runtime permission dialog, and it lets you query which permissions are currently granted. That removes the blind spot for denial. It adds two obligations: you must ship a rationale screen explaining why you want each type, and you must handle the case where Health Connect is not installed or is too old on the device.

The trap is the same on both platforms: an empty chart is not evidence of a missing permission, and a missing permission is not evidence of an empty chart. Never infer grant state from empty results. On Health Connect, read grant state explicitly. On HealthKit, record what you asked for, show the user which types are flowing, and give them a route to the Health app's settings when a type they expect is silent.

On both platforms you need to explain the request before you make it. A permission sheet listing a dozen clinical-sounding types with no context produces denials, and the denials are hard or impossible to reverse from inside your app.

Getting data off the device

With a cloud provider, your server pulls whenever it wants. With an on-device store, the app pushes whenever it can. Everything below follows from that inversion.

Noticing new data

On iOS, register an observer query per type and enable background delivery. When new samples arrive the system may launch your app in the background, at which point you run an anchored query from the last anchor you stored and receive only the new and deleted samples. The wake-up frequency is throttled by the system, so treat it as best effort.

On Android there is no push. Schedule periodic background work, and on each run ask Health Connect for changes since the change token you stored last time. Tokens can expire if you wait too long between reads, in which case you fall back to a bounded time-range read.

Moving it to your backend

  • Assume the network is absent. Queue records locally, upload in batches, and retry with backoff. A sync that happens on the next app open is normal, not a failure.
  • Make upload idempotent. Key each record on the store's own record identifier plus the user. Retries and overlapping anchors will resend records; your backend must treat a resend as a no-op.
  • Carry deletions. Both stores let users delete samples. An anchored query on iOS and a changes read on Android both report deletions, and your backend needs a way to receive them.
  • Budget the first connect. The initial read is the expensive one: a user with years of Apple Watch history can have millions of samples. Backfill in chunks, oldest first or newest first depending on what your product shows, and cap how far back you go unless the user asks for more.

The record identifier is the piece people forget. The payload below is the shape the WearLink sync endpoint accepts; note that every item carries the on-device identifier and its source, because both are needed later.

POST /sdk/users/{user_id}/sync
{
  "provider": "apple",
  "sdkVersion": "1.0.0",
  "syncTimestamp": "2026-09-01T06:00:00Z",
  "data": {
    "records": [
      {
        "id": "hk-record-uuid",
        "type": "HKQuantityTypeIdentifierStepCount",
        "startDate": "2026-09-01T05:00:00Z",
        "endDate": "2026-09-01T06:00:00Z",
        "value": 1240,
        "unit": "count",
        "source": { "deviceName": "Apple Watch", "deviceType": "watch" }
      }
    ],
    "workouts": [],
    "sleep": []
  }
}

Duplicates are the normal case

Both stores are aggregators in their own right. Every app the user allows can write into them, and most wearable companion apps do. The result is that the same physiological event exists several times, and your pipeline will see all of them:

  • An Apple Watch wearer who also wears an Oura ring has two sleep records for every night in Apple Health, one from watchOS and one written by the Oura app, with different stage boundaries.
  • A Garmin user on Android has Garmin Connect writing workouts and daily steps into Health Connect, alongside the phone's own step counter.
  • A run recorded on a watch, synced to Strava, and written back to the on-device store arrives at your backend from the store via your app and again from Strava via its cloud API, if you connect both.

Deleting duplicates is the wrong instinct, because you cannot know at write time which copy the user regards as canonical. The workable rule has two parts:

  1. Keep the origin on every record. HealthKit tags samples with the source app's bundle identifier and device; Health Connect tags records with a data origin. Store it. It is the only reliable way to tell "Oura's sleep" from "the Watch's sleep" later.
  2. Apply a provider-priority rule at read time. When two records of the same type overlap in time for the same user, prefer the origin with the higher priority and mark the other as a duplicate. Which provider wins is a product decision (the dedicated sleep device usually beats the watch for sleep, the watch usually beats the phone for steps), but it must be one rule applied everywhere, not a per-feature special case.

This is also why a step count from the on-device store should land in the same table, in the same shape, as a step count from Fitbit. If they are different objects, your priority rule has to be written twice.

What an aggregator can and cannot do

An aggregator cannot give you a cloud API for HealthKit or Health Connect. There is no server to proxy. If you have no mobile app, no vendor can get you Apple Health or Health Connect data, full stop.

What an aggregator can do is take the device side off your plate and make the output indistinguishable from its cloud providers: SDKs that read the on-device store, handle anchors and batching, and push into the same normalised pipeline as everything else, so a steps.created event from Health Connect and one from Fitbit are the same object with a different source, and the provider-priority rule above runs once.

WearLink's position on 1 September 2026, stated plainly:

StoreStatusWhat exists today
Apple HealthOn-device (SDK)Swift SDK that reads HealthKit and pushes batches of records, sleep and workouts to the sync endpoint using a short-lived per-user SDK token minted by your backend. Ships as source; package-registry publishing is in progress.
Google Health ConnectOn-device (SDK, preview)Kotlin/JVM SDK published as source. The current preview surface is deliberately narrow (provider listing, user connections, activity summaries, meal-photo upload) and is expanding; Maven Central publishing is staged.
Samsung HealthOn-device (SDK)SDK push path into the shared schema. Samsung partner approval is required for the SDK itself; on recent Galaxy devices Health Connect is often the simpler route.
Apple Health XML exportImport (one-off)Upload the ZIP the Health app exports, directly for small archives or via a presigned upload for large ones. Processing is asynchronous and emits the same normalised events (workout.created, sleep.created and so on) as records land. Useful for historical loads without a HealthKit permission round-trip.
Every row above is an on-device path. All of them require you to ship your own iOS or Android app and to obtain permission from the user inside it. The XML import is the one exception to needing an SDK, but it still needs the user to export the file from their own phone. Details are in the docs.

Decision guide

  • No mobile app, or none planned: skip both stores. Use the cloud OAuth providers you can connect today (Oura, WHOOP, Fitbit, Strava, Withings), which your server reads without the phone being involved. Garmin, Polar, Suunto, Ultrahuman, Dexcom and Eight Sleep are in provider onboarding.
  • iOS app, need Apple Watch data: HealthKit via the Swift SDK. Budget the permission UX and the first-connect backfill. Plan for silent denials.
  • Android app, currently on Google Fit: you are on a deprecation clock. Follow the Google Fit migration guide, export history first, then move reads to Health Connect.
  • Android app, many Galaxy users: start with Health Connect and check what Samsung Health already surfaces there before committing to the Samsung SDK and its partner approval.
  • Both stores plus cloud providers: decide the provider-priority rule before you write the first record, and keep the data origin on everything. Retrofitting deduplication onto a year of mixed data is far harder than designing for it.
  • Historical Apple Health data for a small cohort: the XML export import is often enough, and needs no app at all beyond a way for the user to hand you the file.

The short version: HealthKit and Health Connect are worth integrating precisely because they aggregate so much, and they are expensive to integrate for exactly the same reason. Neither will ever be a cloud API. Build for a device that is sometimes offline, a user who sometimes says no, and a record that usually exists twice.

FAQ

Frequently asked

Is there an Apple Health cloud API?
No. HealthKit is an on-device framework. Apple does not operate a server that returns a user’s health data, so the only way to read it is code running inside an iOS app the user has installed and granted permission to. Any product describing "Apple Health via OAuth" is describing something that does not exist.
Is there a Health Connect cloud API?
No. Health Connect is an on-device store on Android. It replaced the Google Fit REST API, which did allow server-side reads, and nothing server-side replaced that capability. An Android app is a prerequisite.
Why does a denied permission look the same as no data?
On HealthKit this is deliberate: a read denial is not reported to the app, so a denied type and an empty type both return nothing. Health Connect exposes grant state, but a granted type with no records still returns nothing. In both cases you must check grant state separately rather than infer it from empty results.
How do I stop the same workout being counted twice?
Keep the source app or data origin on every record, then apply a provider-priority rule: when the same activity arrives from the on-device store and from its original provider (for example Strava), keep the higher-priority copy and mark the other as a duplicate rather than deleting it.
What can WearLink do for Apple Health and Health Connect if there is no cloud API?
Provide the device side and the normalisation. A Swift SDK reads HealthKit and pushes records to the sync endpoint, a Kotlin SDK is in preview for Android, Samsung Health is supported via SDK, and the Apple Health XML export can be imported for one-off historical loads. All of these require you to ship your own mobile app.

One pipeline for on-device and cloud providers

Push HealthKit and Health Connect records from your app and receive them in the same shape as Oura, WHOOP, Fitbit, Strava and Withings.