> ## Documentation Index
> Fetch the complete documentation index at: https://setup.despia.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Apple Health

> Read and write Apple HealthKit data including steps, heart rate, sleep, workouts, and more within your Despia app.

Access Apple HealthKit from your web app through the Despia native bridge. Despia gives you three ways to work with health data: read historical records on demand, write new samples back to HealthKit, and subscribe to live updates via server webhooks. All three work from JavaScript with no native code required.

<Info>
  HealthKit is iOS only. Always gate calls behind an `isDespiaIOS` check so your app degrades gracefully in a browser or on Android.
</Info>

***

## Installation

<Tabs>
  <Tab title="Bundle">
    <CodeGroup>
      ```bash npm theme={null}
      npm install despia-native
      ```

      ```bash pnpm theme={null}
      pnpm add despia-native
      ```

      ```bash yarn theme={null}
      yarn add despia-native
      ```
    </CodeGroup>

    ```javascript theme={null}
    import despia from 'despia-native';
    ```
  </Tab>

  <Tab title="CDN">
    <CodeGroup>
      ```html UMD theme={null}
      <script src="https://cdn.jsdelivr.net/npm/despia-native/index.min.js"></script>
      ```

      ```html ESM theme={null}
      <script type="module">
          import despia from 'https://cdn.jsdelivr.net/npm/despia-native/+esm'
      </script>
      ```
    </CodeGroup>
  </Tab>
</Tabs>

***

## Apple Developer and Despia setup

HealthKit requires the HealthKit capability on your core app bundle ID before any read or write call works. Configure Apple Developer first, then enable Health Data in the Despia Editor and rebuild.

<Steps>
  <Step title="Sign in to Apple Developer">
    Go to [developer.apple.com](https://developer.apple.com) and sign in with the Apple Developer account that owns your app's primary bundle ID. Navigate to **Certificates, Identifiers & Profiles > Identifiers**.
  </Step>

  <Step title="Add HealthKit to your core app bundle ID">
    Find your core app (e.g. `com.despia.myapp`), click into it, and check **HealthKit** under Capabilities. Click **Save**. Without this, HealthKit reads will return empty arrays and writes will fail silently, since iOS rejects HealthKit access for any app whose bundle ID does not declare the capability.
  </Step>

  <Step title="Enable Health Data in the Despia Editor">
    Open the Despia Editor and go to **App > Addons > Health Data**. Toggle the addon on. This signals Despia to compile the HealthKit framework and the matching usage descriptions into your next build.
  </Step>

  <Step title="Rebuild your app">
    Trigger a fresh build from the Despia Editor. HealthKit linking happens at the binary level, so this cannot be applied over-the-air. After the rebuild, calls to `readhealthkit://`, `writehealthkit://`, `healthkit://workouts`, and `healthkit://observe` will function in production.
  </Step>
</Steps>

<Warning>
  Skipping the HealthKit capability on the core bundle ID, or skipping the rebuild after enabling the Despia addon, leaves the integration inactive even when both toggles read enabled. `readhealthkit://` returns empty arrays, `healthkit://workouts` resolves to `[]`, `writehealthkit://` resolves silently without saving, and `healthkit://observe` never fires a webhook. If health data stops working after editing settings, verify the capability is on in Apple Developer and rebuild before opening a support ticket.
</Warning>

***

## Choosing the right approach

HealthKit data can be accessed in three ways. Which one to use depends on whether you need data right now, want to save data, or need to react to changes continuously.

| Approach     | Use when                                                                                                                   | How it works                                                                                                                           |
| :----------- | :------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------- |
| **Read**     | You need historical data at a specific moment, on page load, on a button tap, or when building a report                    | Call `readhealthkit://` or `healthkit://workouts` and await the result. Returns records for the last N days                            |
| **Write**    | You want to save a value back to HealthKit, for example logging a weight entry or a step count from your own sensor        | Call `writehealthkit://` with a value. Adds a new sample without replacing existing data                                               |
| **Observer** | You need your server to stay in sync with the user's health data automatically, without requiring the user to open the app | Register a webhook with `healthkit://observe`. Despia posts new data to your server whenever HealthKit updates, even in the background |

A fitness app that shows a weekly steps chart uses **read**, fetching data when the user opens the dashboard. A coaching platform that tracks whether a user hit their daily goal uses **observer**, the server receives updates automatically and can send a push notification without any user action. An app that lets users log body weight from a form uses **write** to save the entry back to Apple Health.

You can combine all three. Most apps that use observer also use read to populate the initial UI before the first webhook fires.

***

## How it works

Define the `isDespiaIOS` check once at the top of your app and reuse it throughout. Pass one or more identifiers as a comma-separated list on the URL host, Despia routes the request through the native bridge in a single dispatch and returns one response keyed by identifier.

```javascript theme={null}
const isDespiaIOS = navigator.userAgent.toLowerCase().includes('despia') && (
    navigator.userAgent.toLowerCase().includes('iphone') ||
    navigator.userAgent.toLowerCase().includes('ipad')
)

if (isDespiaIOS) {
    const data  = await despia(
        'readhealthkit://HKQuantityTypeIdentifierStepCount,HKQuantityTypeIdentifierHeartRate?days=7',
        ['healthkitResponse']
    )

    const steps = data.healthkitResponse.HKQuantityTypeIdentifierStepCount
    const hr    = data.healthkitResponse.HKQuantityTypeIdentifierHeartRate
}
```

Despia requests HealthKit permission on the first call for each identifier, then fetches and returns the data. A single multi-identifier call prompts the user once for the whole set rather than once per type.

***

## Read

Use `readhealthkit://` to fetch historical data on demand. This is the right approach when your UI needs to display health data at a point in time, on load, after a user action, or when generating a report. You control exactly when the fetch happens and how many days of history to retrieve.

Pass any valid `HKQuantityTypeIdentifier`, `HKCategoryTypeIdentifier`, `HKWorkoutTypeIdentifier`, or `HKCharacteristicTypeIdentifier` as the URL host, plus an optional `days` parameter. See [Identifier reference](#identifier-reference) for the full list.

### Reading multiple identifiers

Most real apps need more than one metric at a time, a dashboard typically wants steps, heart rate, calories, and sleep all at once. Comma-separate the identifiers on the URL host and Despia returns them in a single response, keyed by identifier name.

```javascript theme={null}
if (isDespiaIOS) {
    const data = await despia(
        'readhealthkit://HKQuantityTypeIdentifierStepCount,HKQuantityTypeIdentifierActiveEnergyBurned,HKCategoryTypeIdentifierSleepAnalysis?days=7',
        ['healthkitResponse']
    )

    const steps  = data.healthkitResponse.HKQuantityTypeIdentifierStepCount
    const energy = data.healthkitResponse.HKQuantityTypeIdentifierActiveEnergyBurned
    const sleep  = data.healthkitResponse.HKCategoryTypeIdentifierSleepAnalysis
}
```

Each key on `healthkitResponse` holds the same array shape that a single-identifier read would return for that type. Quantity types return daily values, sleep returns stage intervals, workouts return session records. The single `days` parameter applies to every identifier in the call.

You can mix categories freely in one call as long as they all accept a `days` window:

```javascript theme={null}
if (isDespiaIOS) {
    const data = await despia(
        'readhealthkit://HKQuantityTypeIdentifierStepCount,HKQuantityTypeIdentifierHeartRate,HKWorkoutTypeIdentifier?days=14',
        ['healthkitResponse']
    )
}
```

Characteristics (`HKCharacteristicTypeIdentifierDateOfBirth`, biological sex, blood type, skin type) are static values and do not accept `days`, so fetch them with their own dedicated call.

<Warning>
  Always batch related reads into one comma-separated call. Firing several `despia()` reads in parallel with `Promise.all` queues multiple independent requests against the native bridge, which can race during permission prompts and on slower devices, causing one or more reads to return stale or empty arrays without throwing. A single multi-identifier call goes through one native dispatch, hits HealthKit's authorization layer once, and resolves all identifiers atomically.
</Warning>

### Quantity types

Quantity types cover numeric metrics: steps, heart rate, distance, body mass, calories, and more. Each record represents one day's value for the requested identifier. For individual samples with timestamps instead of a daily value, see [Raw samples](#raw-samples).

```javascript theme={null}
if (isDespiaIOS) {
    const data      = await despia('readhealthkit://HKQuantityTypeIdentifierHeartRate?days=30', ['healthkitResponse'])
    const heartRate = data.healthkitResponse.HKQuantityTypeIdentifierHeartRate
}
```

```json theme={null}
[
  { "date": "2025-11-16", "value": 68, "unit": "count/min" },
  { "date": "2025-11-17", "value": 72, "unit": "count/min" },
  { "date": "2025-11-18", "value": 70, "unit": "count/min" }
]
```

<ResponseField name="date" type="string">
  Start of day in ISO 8601 format
</ResponseField>

<ResponseField name="value" type="number">
  The health metric value for that day
</ResponseField>

<ResponseField name="unit" type="string">
  Unit of measurement, e.g. `count`, `count/min`, `kg`, `m`. Determined automatically by the identifier.
</ResponseField>

***

### Raw samples

By default a quantity read returns one averaged value per day. Add `raw=true` to get every individual sample with its own timestamp instead. The daily average is computed on-device, so without `raw` the underlying samples never reach your app. This is the mode to use for intra-day or overnight analysis, for example tracking how HRV moves across a night of sleep.

```javascript theme={null}
if (isDespiaIOS) {
    const data    = await despia('readhealthkit://HKQuantityTypeIdentifierHeartRateVariabilitySDNN?days=1&raw=true', ['healthkitResponse'])
    const samples = data.healthkitResponse.HKQuantityTypeIdentifierHeartRateVariabilitySDNN
}
```

```json theme={null}
[
  { "startDate": "2026-06-03T01:14:23Z", "endDate": "2026-06-03T01:14:23Z", "value": 52.3, "unit": "ms" },
  { "startDate": "2026-06-03T01:19:41Z", "endDate": "2026-06-03T01:19:41Z", "value": 48.7, "unit": "ms" },
  { "startDate": "2026-06-03T01:24:58Z", "endDate": "2026-06-03T01:24:58Z", "value": 61.2, "unit": "ms" }
]
```

<ResponseField name="startDate" type="string">
  ISO 8601 start time of the sample
</ResponseField>

<ResponseField name="endDate" type="string">
  ISO 8601 end time of the sample
</ResponseField>

<ResponseField name="value" type="number">
  The measured value for that sample
</ResponseField>

<ResponseField name="unit" type="string">
  Unit of measurement, determined automatically by the identifier
</ResponseField>

`raw=true` works with comma-separated identifiers and multi-day windows, and applies to quantity types only. Samples come back sorted oldest to newest. It has no effect on `HKCategoryTypeIdentifierSleepAnalysis` or workout reads, which already return per-sample data. Calls without `raw=true` return the same daily aggregate as before, so adding it is non-breaking, and on an app build that predates this feature `raw=true` is ignored and you get the aggregate.

***

### Sleep data

Sleep analysis uses `HKCategoryTypeIdentifierSleepAnalysis`. Unlike quantity types, sleep returns individual stage intervals rather than daily aggregates, each record covers a contiguous block of one sleep stage with its own start and end time.

```javascript theme={null}
if (isDespiaIOS) {
    const data  = await despia('readhealthkit://HKCategoryTypeIdentifierSleepAnalysis?days=7', ['healthkitResponse'])
    const sleep = data.healthkitResponse.HKCategoryTypeIdentifierSleepAnalysis
}
```

```json theme={null}
[
  { "startDate": "2025-11-17T22:15:00Z", "endDate": "2025-11-17T22:45:00Z", "value": 0, "label": "inBed" },
  { "startDate": "2025-11-17T22:45:00Z", "endDate": "2025-11-18T00:30:00Z", "value": 3, "label": "core" },
  { "startDate": "2025-11-18T00:30:00Z", "endDate": "2025-11-18T02:15:00Z", "value": 4, "label": "deep" },
  { "startDate": "2025-11-18T02:15:00Z", "endDate": "2025-11-18T03:45:00Z", "value": 5, "label": "rem"  },
  { "startDate": "2025-11-18T03:45:00Z", "endDate": "2025-11-18T06:00:00Z", "value": 3, "label": "core" },
  { "startDate": "2025-11-18T06:00:00Z", "endDate": "2025-11-18T06:20:00Z", "value": 2, "label": "awake" }
]
```

<ResponseField name="startDate" type="string">
  ISO 8601 start time of the sleep stage interval
</ResponseField>

<ResponseField name="endDate" type="string">
  ISO 8601 end time of the sleep stage interval
</ResponseField>

<ResponseField name="value" type="number">
  Raw integer from `HKCategoryValueSleepAnalysis`
</ResponseField>

<ResponseField name="label" type="string">
  Human-readable stage: `inBed`, `awake`, `asleep`, `core`, `deep`, or `rem`
</ResponseField>

***

### Workouts

Use the dedicated `healthkit://workouts` scheme to fetch workout sessions, optionally enriched with per-workout statistics like average heart rate or summed active energy. The response lands on `window.healthkitWorkouts` as a flat array, distinct from the multi-type `healthkitResponse` object used by `readhealthkit://`.

```javascript theme={null}
if (isDespiaIOS) {
    const data     = await despia('healthkit://workouts?days=14', ['healthkitWorkouts'])
    const workouts = data.healthkitWorkouts
}
```

```json theme={null}
[
  {
    "date": "2026-05-07T07:32:11Z",
    "activityType": "Running",
    "duration": 1820.4,
    "calories": 312.7,
    "distance": 4521.3,
    "value": 4521.3,
    "unit": "m"
  },
  {
    "date": "2026-05-05T18:02:00Z",
    "activityType": "FunctionalStrengthTraining",
    "duration": 2700,
    "calories": 220.5,
    "distance": 0,
    "value": 220.5,
    "unit": "kcal"
  }
]
```

<ParamField path="days" type="number">
  Number of days back to look. Defaults to `1`.
</ParamField>

<ParamField path="included" type="string">
  Comma-separated list of per-workout quantity statistics. Each entry is a real `HKQuantityTypeIdentifier` followed by an aggregation suffix. Omit to skip per-workout stats. See [Per-workout statistics](#per-workout-statistics).
</ParamField>

<ResponseField name="date" type="string">
  ISO 8601 start time of the workout
</ResponseField>

<ResponseField name="activityType" type="string">
  Apple's activity enum name in PascalCase, for example `Running`, `Cycling`, `FunctionalStrengthTraining`. See [HKWorkoutActivityType](https://developer.apple.com/documentation/healthkit/hkworkoutactivitytype) for the complete list.
</ResponseField>

<ResponseField name="duration" type="number">
  Duration in seconds
</ResponseField>

<ResponseField name="calories" type="number">
  Kilocalories burned. `0` if the source did not record this.
</ResponseField>

<ResponseField name="distance" type="number">
  Distance in meters. `0` if not applicable, for example strength training.
</ResponseField>

<ResponseField name="value" type="number">
  Primary metric, picked dynamically: distance if greater than `0`, otherwise calories, otherwise duration in seconds
</ResponseField>

<ResponseField name="unit" type="string">
  Unit of `value`: `m`, `kcal`, or `s`
</ResponseField>

<ResponseField name="samples" type="array">
  Per-workout quantity statistics, present only when `included=` is non-empty. Each entry has `key`, `value`, and `unit`.
</ResponseField>

#### Per-workout statistics

Pass `included=` to compute aggregated quantity values scoped to each workout's time range. Each entry is an `HKQuantityTypeIdentifier` plus an aggregation suffix. The native side strips the suffix to request authorization for the underlying type, then runs the aggregation across that workout's `startDate` to `endDate` window. The result is the avg, max, min, or sum of that signal during the workout, not over the whole day.

| Suffix    | Aggregation      | Example                                         |
| :-------- | :--------------- | :---------------------------------------------- |
| `Average` | Discrete average | `HKQuantityTypeIdentifierHeartRateAverage`      |
| `Max`     | Discrete maximum | `HKQuantityTypeIdentifierHeartRateMax`          |
| `Min`     | Discrete minimum | `HKQuantityTypeIdentifierHeartRateMin`          |
| `Sum`     | Cumulative sum   | `HKQuantityTypeIdentifierActiveEnergyBurnedSum` |
| *(none)*  | Discrete average | `HKQuantityTypeIdentifierHeartRate`             |

Stack multiple stats for the same identifier by repeating it with different suffixes. The example below pulls average, max, and min heart rate for every workout in the last 14 days.

```javascript theme={null}
if (isDespiaIOS) {
    const data     = await despia(
        'healthkit://workouts?days=14&included=HKQuantityTypeIdentifierHeartRateAverage,HKQuantityTypeIdentifierHeartRateMax,HKQuantityTypeIdentifierHeartRateMin',
        ['healthkitWorkouts']
    )
    const workouts = data.healthkitWorkouts
}
```

```json theme={null}
[
  {
    "date": "2026-05-07T07:32:11Z",
    "activityType": "Running",
    "duration": 1820.4,
    "calories": 312.7,
    "distance": 4521.3,
    "value": 4521.3,
    "unit": "m",
    "samples": [
      { "key": "HKQuantityTypeIdentifierHeartRateAverage", "value": "148.2", "unit": "count/min" },
      { "key": "HKQuantityTypeIdentifierHeartRateMax",     "value": "176.0", "unit": "count/min" },
      { "key": "HKQuantityTypeIdentifierHeartRateMin",     "value": "62.0",  "unit": "count/min" }
    ]
  }
]
```

Mix multiple identifiers and aggregations in a single call. Heart rate stats and summed active energy together:

```javascript theme={null}
if (isDespiaIOS) {
    const data     = await despia(
        'healthkit://workouts?days=7&included=HKQuantityTypeIdentifierHeartRateAverage,HKQuantityTypeIdentifierHeartRateMax,HKQuantityTypeIdentifierActiveEnergyBurnedSum',
        ['healthkitWorkouts']
    )
    const workouts = data.healthkitWorkouts
}
```

```json theme={null}
{
  "date": "2026-05-04T06:10:00Z",
  "activityType": "Cycling",
  "duration": 3600,
  "calories": 540.2,
  "distance": 18234.0,
  "value": 18234.0,
  "unit": "m",
  "samples": [
    { "key": "HKQuantityTypeIdentifierHeartRateAverage",      "value": "138.4", "unit": "count/min" },
    { "key": "HKQuantityTypeIdentifierHeartRateMax",          "value": "171.0", "unit": "count/min" },
    { "key": "HKQuantityTypeIdentifierActiveEnergyBurnedSum", "value": "540.2", "unit": "kcal" }
  ]
}
```

Common combinations that work well:

| Goal                             | `included=` value                                                                                                    |
| :------------------------------- | :------------------------------------------------------------------------------------------------------------------- |
| Heart rate range per workout     | `HKQuantityTypeIdentifierHeartRateAverage,HKQuantityTypeIdentifierHeartRateMax,HKQuantityTypeIdentifierHeartRateMin` |
| Total energy per workout         | `HKQuantityTypeIdentifierActiveEnergyBurnedSum`                                                                      |
| Pace and top speed for runs      | `HKQuantityTypeIdentifierRunningSpeedAverage,HKQuantityTypeIdentifierRunningSpeedMax`                                |
| Average and peak power for rides | `HKQuantityTypeIdentifierCyclingPowerAverage,HKQuantityTypeIdentifierCyclingPowerMax`                                |

<ResponseField name="samples[].key" type="string">
  Exact identifier you passed in `included=`, including the aggregation suffix
</ResponseField>

<ResponseField name="samples[].value" type="string">
  Stringified numeric result. Wrap with `Number()` before doing math. A value of `"0"` means the source did not record that signal during the workout, treat as missing rather than literal zero.
</ResponseField>

<ResponseField name="samples[].unit" type="string">
  HealthKit unit string, for example `count/min`, `kcal`, `m`
</ResponseField>

<Info>
  Authorization for every base quantity type referenced in `included=` is requested automatically on the first call, the suffix is stripped before the auth request. Invalid identifiers in `included=` are silently skipped, valid ones in the same call still come through. If the user denies access or the build does not have HealthKit linked, `window.healthkitWorkouts` resolves to `[]`, so always check `Array.isArray()` before reading.
</Info>

The legacy `readhealthkit://HKWorkoutTypeIdentifier` endpoint continues to work and is the right choice when you need workouts in the same call as other unrelated identifiers, for example pulling steps, heart rate, and workouts together. Its response shape is slightly different: it lands on `healthkitResponse.HKWorkoutTypeIdentifier` rather than `healthkitWorkouts`, returns lowercase `activityType` values, and does not support `included=`. Use `healthkit://workouts` for any workouts-only read or any read that needs per-workout statistics.

***

### Characteristics

Characteristics such as date of birth, biological sex, and blood type are static values that do not change over time. They return a plain string or raw integer and do not accept a `days` parameter, so they cannot be batched into the same call as quantity, category, or workout reads.

```javascript theme={null}
if (isDespiaIOS) {
    const data = await despia('readhealthkit://HKCharacteristicTypeIdentifierDateOfBirth', ['healthkitResponse'])
    const dob  = data.healthkitResponse.HKCharacteristicTypeIdentifierDateOfBirth
}
```

```json theme={null}
"1990-06-15T00:00:00Z"
```

| Identifier                                          | Returns                                  |
| :-------------------------------------------------- | :--------------------------------------- |
| `HKCharacteristicTypeIdentifierDateOfBirth`         | ISO 8601 date string                     |
| `HKCharacteristicTypeIdentifierBiologicalSex`       | Raw integer from `HKBiologicalSex`       |
| `HKCharacteristicTypeIdentifierBloodType`           | Raw integer from `HKBloodType`           |
| `HKCharacteristicTypeIdentifierFitzpatrickSkinType` | Raw integer from `HKFitzpatrickSkinType` |

***

## Write

Use `writehealthkit://` to save a value back to HealthKit. This is useful when your app collects health data that should also live in Apple Health, for example a weight logging form, a manual step entry, or a calorie tracker. Writing adds a new sample to HealthKit without removing or replacing any existing records.

The URL format is `writehealthkit://IdentifierString//Value`. The unit is resolved automatically from the identifier.

```javascript theme={null}
if (isDespiaIOS) {
    despia('writehealthkit://HKQuantityTypeIdentifierBodyMass//74.5')
    despia('writehealthkit://HKQuantityTypeIdentifierStepCount//10000')
}
```

<ParamField path="identifier" type="string" required>
  Any writable `HKQuantityTypeIdentifier`, passed as the URL host (e.g. `HKQuantityTypeIdentifierBodyMass`)
</ParamField>

<ParamField path="value" type="number" required>
  Numeric value to write. The unit is determined automatically from the identifier, see the [Identifier reference](#identifier-reference).
</ParamField>

<Info>
  Write is one-directional. It adds samples to HealthKit but does not delete or modify existing ones. If you need to correct a previously written value, write a new sample, HealthKit stores the history and surfaces the most recent reading.
</Info>

***

## Observer

Use observers when you need your server to stay current with a user's health data automatically, without requiring the user to open your app. Observers register a background listener on the device that watches one or more HealthKit types. When new data arrives, from Apple Watch, a connected sensor, or another app, Despia posts a webhook to your server with the latest records.

This is fundamentally different from read. Read is a point-in-time fetch that your code initiates. An observer is a persistent subscription that fires on Despia's side whenever HealthKit tells it something changed. The user does not need to be in the app for a webhook to fire.

Common uses for observers include: tracking whether a user hit a daily step goal, alerting a coach when a client logs a workout, syncing sleep data nightly to a backend, and triggering push notifications based on health events.

### Registering an observer

```javascript theme={null}
if (isDespiaIOS) {
    despia('healthkit://observe?types=HKQuantityTypeIdentifierStepCount,HKCategoryTypeIdentifierSleepAnalysis&frequency=hourly&server=https://your-server.com/webhook')
}
```

Observers persist across app restarts automatically. You only need to register them once, typically on app load or after the user enables health tracking in your app.

<ParamField path="types" type="string" required>
  Comma-separated list of HealthKit identifiers to observe
</ParamField>

<ParamField path="frequency" type="string">
  How often Despia delivers batched updates to your server: `immediate`, `hourly`, `daily`, or `weekly`. Defaults to `immediate`. Use `hourly` or `daily` for high-volume types like steps to avoid unnecessary webhook traffic.
</ParamField>

<ParamField path="server" type="string" required>
  Full URL of the endpoint that receives webhook POSTs. Append your own query parameters here to identify the user on your server.
</ParamField>

### Identifying users in webhooks

Webhooks include a `userId` field containing the Despia device ID. If you need to map webhook events to your own user records, append your user identifier as a query parameter on the `server` URL when registering the observer. Your server receives it as part of the request URL.

```javascript theme={null}
const myUserId = 'user_abc123'

if (isDespiaIOS) {
    despia(`healthkit://observe?types=HKQuantityTypeIdentifierStepCount&frequency=hourly&server=https://your-server.com/webhook?user=${myUserId}`)
}
```

Your server receives `POST /webhook?user=user_abc123` and can route the event directly to the right user record without any device ID mapping.

### Webhook payload

Despia sends a `POST` with `Content-Type: application/json` each time an observed type updates. The `data` object contains one key per observed type, each holding an array of the most recent records (last 1 day).

```json theme={null}
{
  "event": "update",
  "userId": "abc123-device-id",
  "timestamp": "2025-11-18T08:00:00Z",
  "data": {
    "HKQuantityTypeIdentifierStepCount": [
      { "date": "2025-11-18", "value": 4210, "unit": "count" }
    ]
  }
}
```

<ResponseField name="event" type="string">
  Always `update`
</ResponseField>

<ResponseField name="userId" type="string">
  Despia device ID, consistent across all events from the same device. For your own user IDs, use a query parameter on the `server` URL instead.
</ResponseField>

<ResponseField name="timestamp" type="string">
  ISO 8601 timestamp of when the webhook was sent
</ResponseField>

<ResponseField name="data" type="object">
  One key per observed type, each containing an array of records matching the read response shape for that type
</ResponseField>

### Checking active observers

`despia.observingHealthKit` holds an array of currently active identifier strings. It is `undefined` before any `observe` or `unobserve` call has been made, and `[]` once all observers have been stopped. Use this to check whether an observer is already registered before calling `observe` again.

```javascript theme={null}
if (despia.observingHealthKit?.length > 0) {
    console.log('Active observers:', despia.observingHealthKit)
    // ["HKQuantityTypeIdentifierStepCount", "HKCategoryTypeIdentifierSleepAnalysis"]
}
```

### Stopping observers

Pass `all` to stop every active observer, or a comma-separated list of identifiers to stop specific types. The response contains the updated list of remaining active observers.

```javascript theme={null}
if (isDespiaIOS) {
    // Stop all observers
    const data = await despia('healthkit://unobserve?types=all', ['observingHealthKit'])
    console.log(data.observingHealthKit) // []

    // Stop a specific type
    const data = await despia('healthkit://unobserve?types=HKQuantityTypeIdentifierStepCount', ['observingHealthKit'])
    console.log(data.observingHealthKit) // ["HKCategoryTypeIdentifierSleepAnalysis"]
}
```

***

## Identifier reference

Despia supports all four HealthKit identifier categories. Pass any valid identifier string directly to `readhealthkit://` or `writehealthkit://`, or as part of `included=` on `healthkit://workouts`. Despia resolves the type and unit automatically.

### Constructing identifiers

Every identifier follows a predictable pattern based on its category:

| Type           | Prefix                           | Example                                     |
| :------------- | :------------------------------- | :------------------------------------------ |
| Quantity       | `HKQuantityTypeIdentifier`       | `HKQuantityTypeIdentifierStepCount`         |
| Category       | `HKCategoryTypeIdentifier`       | `HKCategoryTypeIdentifierSleepAnalysis`     |
| Workout        | `HKWorkoutTypeIdentifier`        | `HKWorkoutTypeIdentifier`                   |
| Characteristic | `HKCharacteristicTypeIdentifier` | `HKCharacteristicTypeIdentifierDateOfBirth` |

Take the type name from Apple's documentation and prepend the matching prefix. For example, Apple lists `StepCount` under `HKQuantityTypeIdentifier`, the full identifier string is `HKQuantityTypeIdentifierStepCount`.

When using a quantity identifier inside `included=` on `healthkit://workouts`, append one of the aggregation suffixes (`Average`, `Max`, `Min`, `Sum`) to control how the value is computed across the workout window. Identifiers used as the URL host on `readhealthkit://` never take a suffix.

### Apple Documentation

<CardGroup cols={2}>
  <Card title="HKQuantityTypeIdentifier" icon="weight-hanging" href="https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifier">
    Steps, heart rate, distance, body mass, nutrition, and all other numeric types
  </Card>

  <Card title="HKCategoryTypeIdentifier" icon="face-sleeping" href="https://developer.apple.com/documentation/healthkit/hkcategorytypeidentifier">
    Sleep analysis, mindfulness, and other category-based types
  </Card>

  <Card title="HKWorkoutActivityType" icon="timer" href="https://developer.apple.com/documentation/healthkit/hkworkoutactivitytype">
    All workout activity types returned in the `activityType` field
  </Card>

  <Card title="HKCharacteristicTypeIdentifier" icon="droplet" href="https://developer.apple.com/documentation/healthkit/hkcharacteristictypeidentifier">
    Date of birth, biological sex, blood type, and skin type
  </Card>
</CardGroup>

***

## Resources

<CardGroup cols={2}>
  <Card title="NPM Package" icon="npm" href="https://www.npmjs.com/package/despia-native">
    despia-native
  </Card>

  <Card title="Support" icon="envelope" href="mailto:support@despia.com">
    [support@despia.com](mailto:support@despia.com)
  </Card>
</CardGroup>
