Guide

Apple Health data in JavaScript

Apple has no Health API you can call from JavaScript: HealthKit only runs on the iPhone. Sync your iPhone with HealthAPI and you can read your data from Node, Bun or Deno with plain fetch, typed from an OpenAPI spec. Code for each is below.

  • Node 18+, Bun, Deno
  • Plain fetch, typed
  • Updated 22 October 2026

HealthAPI: Free for 2 days, no card needed. Then an optional 7-day free trial when you subscribe.

week.mjs
// week.mjs
import { healthapi } from "./healthapi.mjs";

const get = healthapi(process.env.HEALTHAPI_KEY);

const trends = await Promise.all([
  get("/query/trends", { type: "stepCount", days: 7 }),
  get("/query/trends", { type: "restingHeartRate", days: 7 }),
]);

// Summed metrics carry the day's value in total, the others in avg.
const days = {};
for (const t of trends) {
  const field = t.aggregation === "sum" ? "total" : "avg";
  for (const day of t.data) days[day.date] = { ...days[day.date], [t.type]: day[field] };
}
console.table(days);
$node week.mjs
┌────────────┬───────────┬──────────────────┐
│ (index)    │ stepCount │ restingHeartRate │
├────────────┼───────────┼──────────────────┤
│ 2026-10-16 │ 7333      │ 56               │
│ 2026-10-17 │ 10871     │ 54               │
│ 2026-10-18 │ 11284     │ 54               │
│ 2026-10-19 │ 9120      │ 55               │
│ 2026-10-20 │ 12408     │ 54               │
│ 2026-10-21 │ 8016      │ 55               │
│ 2026-10-22 │ 2310      │ 53               │
└────────────┴───────────┴──────────────────┘

Before you start

Install HealthAPI on your iPhone and let it sync. Copy your key from the app (Connect, then Other tools) and keep it in an environment variable, not in your code:

Shell
export HEALTHAPI_KEY="your key"

A small client

One function for any endpoint. It sends the key as a Bearer token, turns error responses into an error you can check, and needs no packages:

healthapi.mjsJavaScript
// healthapi.mjs: Node 18 or later, Bun and Deno all have fetch built in.
const API = "https://healthapi.app/api/v1";

export class HealthApiError extends Error {
  constructor(status, body, retryAfter) {
    super(typeof body.detail === "string" ? body.detail : `HealthAPI returned ${status}`);
    this.status = status;
    this.body = body;
    this.retryAfter = retryAfter; // seconds, on a 429
  }
}

export function healthapi(key) {
  if (!key) throw new Error("Set HEALTHAPI_KEY to your HealthAPI key.");
  return async function get(path, params = {}) {
    const url = new URL(API + path);
    for (const [name, value] of Object.entries(params)) url.searchParams.set(name, String(value));
    const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } });
    const body = await res.json().catch(() => ({}));
    if (!res.ok) {
      throw new HealthApiError(res.status, body, Number(res.headers.get("Retry-After")) || null);
    }
    return body;
  };
}

A week in a table

The trends endpoint gives one metric a day. Its aggregation field says where the day's value is: total for steps and energy, avg for heart rate, HRV and the like.

JavaScript
// week.mjs
import { healthapi } from "./healthapi.mjs";

const get = healthapi(process.env.HEALTHAPI_KEY);

const trends = await Promise.all([
  get("/query/trends", { type: "stepCount", days: 7 }),
  get("/query/trends", { type: "restingHeartRate", days: 7 }),
]);

// Summed metrics carry the day's value in total, the others in avg.
const days = {};
for (const t of trends) {
  const field = t.aggregation === "sum" ? "total" : "avg";
  for (const day of t.data) days[day.date] = { ...days[day.date], [t.type]: day[field] };
}
console.table(days);

The same file runs in all three. Deno needs permission for the network and the one variable:

Shell
node week.mjs
bun week.mjs
deno run --allow-net=healthapi.app --allow-env=HEALTHAPI_KEY week.mjs
Output (example)Text
┌────────────┬───────────┬──────────────────┐
│ (index)    │ stepCount │ restingHeartRate │
├────────────┼───────────┼──────────────────┤
│ 2026-10-16 │ 7333      │ 56               │
│ 2026-10-17 │ 10871     │ 54               │
│ 2026-10-18 │ 11284     │ 54               │
│ 2026-10-19 │ 9120      │ 55               │
│ 2026-10-20 │ 12408     │ 54               │
│ 2026-10-21 │ 8016      │ 55               │
│ 2026-10-22 │ 2310      │ 53               │
└────────────┴───────────┴──────────────────┘

Today is a partial day, and days with no data are left out of data. Daily and weekly summaries, sleep, workouts and raw readings are in the API reference.

TypeScript types from the OpenAPI spec

The whole API is described in an OpenAPI 3.1 file. Generate types from it, and openapi-fetch checks paths, parameters and responses as you type, metric names included:

Shell
npm install openapi-fetch
npx openapi-typescript https://healthapi.app/openapi.json -o api.d.ts
hrv.mtsTypeScript
// hrv.mts
import createClient from "openapi-fetch";
import type { paths } from "./api.d.ts";

const api = createClient<paths>({
  baseUrl: "https://healthapi.app/api/v1",
  headers: { Authorization: `Bearer ${process.env.HEALTHAPI_KEY}` },
});

const { data, error, response } = await api.GET("/query/trends", {
  params: { query: { type: "heartRateVariability", days: 30 } },
});
if (error) throw new Error(`HealthAPI ${response.status}: ${JSON.stringify(error.detail)}`);

for (const day of data.data) console.log(day.date, day.avg);

Bun runs it as is, and so does Node 24 (node hrv.mts). The .mts ending makes it a module wherever it lives, and the generated file is only types, so it adds nothing at run time.

Errors to handle

StatusMeaningWhat to do
401The key is missing or wrong.Copy it again from the app. A regenerated key replaces the old one.
402No active plan: the free preview ended or the subscription lapsed. The body has code subscription_required.Choose a plan in the app. Synced data is kept for about 5 weeks; don't retry in a loop.
429The day's requests are used up. The body has limit and reset, with a Retry-After header.Wait until the reset at 00:00 UTC.
400, 422A bad date, timezone, metric or missing parameter.Read detail, fix the request.
JavaScript
import { healthapi, HealthApiError } from "./healthapi.mjs";

const get = healthapi(process.env.HEALTHAPI_KEY);

try {
  const today = await get("/query/daily-summary");
  console.log(`${today.steps} steps so far today`);
} catch (e) {
  if (!(e instanceof HealthApiError)) throw e;
  if (e.status === 401) console.error("The key is wrong or missing. Copy it again from the app.");
  else if (e.status === 402) console.error("This account has no active plan:", e.body.code);
  else if (e.status === 429) console.error(`${e.message} Retry in ${e.retryAfter ?? 60} seconds.`);
  else console.error(e.status, e.message);
}

A 429 can also mean too many requests a minute from one address, or too many wrong keys; then the body has only detail.

Daily limits

PlanRequests a day
Free preview (2 days)25
Pro100
Developer1,000

Each health read is one request, and the count resets at 00:00 UTC. Health reads also return X-RateLimit-Remaining. To check without spending one:

JavaScript
const usage = await get("/usage"); // free: doesn't count toward the limit
console.log(`${usage.remaining} of ${usage.limit} left, resets ${usage.reset_at}`);

Not in the browser

Call the API from server-side code only. A key in browser JavaScript can be read by anyone who opens the page, and it unlocks everything you've synced. The API won't answer other websites' pages anyway: it only allows HealthAPI's own site, so the browser blocks the response. Fetch on a server, in a serverless function or at build time, and send the page only the numbers it shows. If a key ever leaks, regenerate it in the app.

For a spreadsheet or an automation instead of code, see the n8n guide; for pandas, the Python guide; to have an AI agent write this code with your real data in view, the Claude Code guide.

Questions

Is there an Apple Health API for JavaScript?

Not from Apple. HealthKit is a framework for apps running on the iPhone and Apple Watch, and Apple offers no web API for Health data. A server, a script or a website has to read it from a service your iPhone syncs to, such as HealthAPI.

Can I use it in Next.js?

Yes, from code that runs on the server: a route handler, a server component or a server action, with the key in an environment variable. Never from a client component, where the key would ship to the browser.

How many requests does the code use?

One per call: each trend covers up to 365 days in one request, so the week table is two. Requests rejected for bad input (400 or 422) count too. GET /usage doesn't count.

Can it tell me when new data arrives?

No, there are no webhooks. Data is as current as your iPhone's last sync, so read it on a schedule, for example once each morning for yesterday.

Which metric names can I use?

HealthKit's own, without the prefix: stepCount, restingHeartRate, heartRateVariability and so on. The metrics page lists all 50, and the generated types autocomplete them.

The HealthAPI app icon

Your Health data, one fetch away.

Get HealthAPI for iPhone, copy your key, and start with the client above.