Quickstart
Install the HealthAPI app for iPhone, authorize Apple Health, and open Connect, then Other tools (or Settings, Sync Settings), to copy your personal key. Then call the API with your key as a Bearer token. The base URL is:
https://healthapi.app/api/v1Every example below is a complete request: drop in your key and run it.
Authentication
All endpoints require your API key in the Authorization header. There are no other credentials: the key is the identity.
Authorization: Bearer <YOUR_API_KEY>Keys are issued by the iPhone app and can be rotated from the Console or via /account/regenerate-key. A missing or invalid key returns 401.
AI agents & automations
Claude, ChatGPT and other MCP clients connect without a key: Claude from its connector directory, the others with the address below, and you approve it in the HealthAPI app on your iPhone. Step-by-step guides: Claude, ChatGPT, and both on one page.
https://healthapi.app/mcp| Client | How |
|---|---|
| Claude | Find HealthAPI in Claude's connector directory (Customize, Connectors, +) and select Connect, or add it by its address as a custom connector. Every plan, including Free. Set it up on the web or desktop and it works in the mobile apps too. |
| ChatGPT | Plugins, +, add a custom MCP server with the address and OAuth, Create as a plugin, then install it and type @ in a chat. Plus, Pro, Business, Enterprise or Edu, on chatgpt.com in a browser on a computer; not Free or Go. |
| Mistral Vibe | Connectors, Add connector, Add a custom connector, with the address as Server; OAuth is detected. Every plan, including Free, at chat.mistral.ai. |
| Claude Code | claude mcp add --transport http healthapi https://healthapi.app/mcp, then /mcp to sign in. |
| Cursor, VS Code | An HTTP MCP server with url set to the address (~/.cursor/mcp.json, .vscode/mcp.json). |
Sign-in is OAuth 2.1 with PKCE, discovered from /.well-known/oauth-protected-resource/mcp. Every tool is read only, and each call counts as one request toward your daily quota:
| Tool | Returns |
|---|---|
get_health_overview | What is synced: every metric with its first and last day, workouts, last sync, timezone, requests left. Start here. |
get_daily_summary | One day: activity, heart, vitals, workouts and the night's sleep. |
get_weekly_summary | A Monday-to-Sunday week: totals, daily averages, a line per day. |
get_sleep | Per night: asleep (core, deep, REM), awake, in bed, bedtime and wake time. |
get_trend | One metric day by day, with average, low and high. |
get_latest | The newest reading of each metric. |
get_workouts | Workouts between two days, with totals. |
get_samples | Raw readings of one metric, up to 31 days. |
Each connection shows in the app under Settings, Connected apps, where you can disconnect it. Regenerating your key disconnects them all.
Anything that can call a REST API works with your key: custom GPTs, n8n, Zapier, Make, Claude Code, Cursor, your own scripts. The whole API is described in an OpenAPI 3.1 spec, and llms.txt sums it up for AI agents.
| Tool | Setup |
|---|---|
| Custom GPT | Configure, Actions, Import from URL: https://healthapi.app/openapi.json. Authentication: API Key, Bearer, your key. Privacy policy: https://healthapi.app/privacy. |
| n8n, Make, Zapier | An HTTP request to an endpoint below with the header Authorization: Bearer <key>, or import the OpenAPI spec where supported. |
| Agents and scripts | Point the agent at https://healthapi.app/llms.txt and give it your key. Call GET /usage first. |
Your key can read everything you've synced, so give it only to tools you trust, and regenerate it in the app to cut them off.
Usage & quota
Returns your plan and how much of your daily quota you've used. This call is cheap, reflects live consumption and doesn't count toward the quota. access_status is trialing, subscribed or expired; when it's expired, health reads return 402.
curl "https://healthapi.app/api/v1/usage" \
-H "Authorization: Bearer $KEY"{
"plan_tier": "pro",
"plan_label": "Pro",
"window": "1d",
"limit": 100,
"used": 7,
"remaining": 93,
"reset_at": "2026-10-13T00:00:00.000Z",
"apple_linked": true,
"access_status": "subscribed",
"trial_ends_at": null,
"deletion_scheduled_at": null,
"locked": false
}Daily summary
Steps, average heart rate, and workouts for a single local day.
| Param | Type | Notes |
|---|---|---|
date | string | YYYY-MM-DD. Defaults to today in your timezone. |
tz | string | IANA zone, e.g. America/New_York. Defaults to your account timezone. |
curl "https://healthapi.app/api/v1/query/daily-summary?date=2026-10-12&tz=America/New_York" \
-H "Authorization: Bearer $KEY"{
"date": "2026-10-12",
"timezone": "America/New_York",
"range_local": {
"start": "2026-10-12T00:00:00.000-04:00",
"end": "2026-10-13T00:00:00.000-04:00"
},
"range_utc": {
"start": "2026-10-12T04:00:00.000Z",
"end": "2026-10-13T04:00:00.000Z"
},
"steps": 11284,
"avg_heart_rate": 62.4,
"workouts": [
{
"type": "running",
"duration_minutes": 41.5,
"calories": 386.4,
"start_date_utc": "2026-10-12T11:02:00.000Z",
"end_date_utc": "2026-10-12T11:43:30.000Z",
"start_date_local": "2026-10-12T07:02:00.000-04:00",
"end_date_local": "2026-10-12T07:43:30.000-04:00"
}
]
}Step totals are aggregated across sources, so overlapping iPhone and Apple Watch samples aren't counted twice, and the number matches Apple Health.
Trends
Per-day statistics for one metric over a window.
| Param | Type | Notes |
|---|---|---|
type | string | Required. e.g. stepCount, heartRate, activeEnergyBurned. |
days | integer | 1–365. Defaults to 7. |
tz | string | IANA zone. Optional. |
curl "https://healthapi.app/api/v1/query/trends?type=heartRate&days=7&tz=UTC" \
-H "Authorization: Bearer $KEY"{
"type": "heartRate",
"timezone": "UTC",
"period_days": 7,
"aggregation": "avg",
"start_date": "2026-10-06",
"end_date": "2026-10-12",
"data": [
{ "date": "2026-10-11", "avg": 64, "min": 48, "max": 142, "total": null, "samples": null },
{ "date": "2026-10-12", "avg": 62, "min": 51, "max": 156, "total": null, "samples": null }
]
}For point-in-time metrics (heart rate, HRV, SpO₂), each day's avg/min/max is served from a pre-computed daily rollup, so long ranges stay fast. Summed metrics (steps, energy, distance) fill total instead. samples can be null on rolled-up days, and days with no data are left out of data.
Full daily aggregate
The complete daily roll-up: steps, active & basal energy, distance, flights, exercise time, heart-rate averages, sleep, and workouts.
| Param | Type | Notes |
|---|---|---|
date | string | Required. YYYY-MM-DD. |
tz | string | IANA zone. Optional. |
curl "https://healthapi.app/api/v1/health/daily-summary?date=2026-10-12" \
-H "Authorization: Bearer $KEY"{
"totals": {
"steps": 11284,
"distance_m": 8600,
"active_energy_kcal": 684,
"basal_energy_kcal": 1633,
"flights_climbed": 9,
"exercise_time_min": 52
},
"averages": {
"heart_rate_bpm": 62,
"resting_heart_rate_bpm": 54,
"hrv_ms": 68,
"spo2_pct": 98,
"respiratory_rate_bpm": 14
},
"sleep": { "total_hours": 7.7, "session_count": 13 },
"workouts": { "count": 1, "total_duration_seconds": 2490, "total_calories_kcal": 386.4 },
"data_coverage_pct": 100,
"sleep_hours": 7.7,
"sleep_minutes": 462,
"sleep_session_count": 13,
"workout_count": 1,
"workout_duration_seconds": 2490,
"workout_duration_minutes": 41.5,
"workout_calories_kcal": 386.4,
"workout_calories": 386.4
}The flat fields after data_coverage_pct are aliases: they carry the same values as the nested sleep and workouts objects, in both hour/second and minute units, so you can map whichever naming your pipeline expects. Mind the units: this endpoint reports sleep in hours and workout duration in seconds, while /query/daily-summary reports each workout in minutes. Reading a minute-named field off an hour-named response is the most common cause of a summary that shows zero or null on real data.
| Field | Unit | Notes |
|---|---|---|
sleep_hours / sleep_minutes | hours / minutes | Time asleep in the night that ended that morning: sleep that ended from 6 pm the day before up to 6 pm that day. Same value as sleep.total_hours. |
sleep_session_count | count | Sleep samples recorded, not nights. Apple Watch writes several stage segments per night. |
workout_count | count | Workouts starting within the local day. |
workout_duration_seconds / workout_duration_minutes | seconds / minutes | Total across all workouts that day. |
workout_calories_kcal / workout_calories | kcal | Active energy across all workouts that day. |
data_coverage_pct | percent | How many of 7 expected daily sources reported: steps, active energy, basal energy, heart rate, distance, exercise time, and sleep. 85 means one is missing. |
Weekly summary
The same aggregate as the full daily roll-up, totalled and averaged across an ISO calendar week.
| Param | Type | Notes |
|---|---|---|
week | string | Required. ISO week in YYYY-Www format, e.g. 2026-W41. |
tz | string | IANA zone. Optional. |
curl "https://healthapi.app/api/v1/health/weekly-summary?week=2026-W41" \
-H "Authorization: Bearer $KEY"Raw samples
Individual data points, for when you want the raw series, not an aggregate. All filters are optional; results are newest-first. Each row has a kind: reading (as Apple Health recorded it; iPhone and Watch readings can overlap), total (the app's deduplicated sum for that period) or daily_average (one day's average). Only total rows are safe to add up. For daily figures, trends and the daily summary are simpler.
Each reading also says where it came from: source_app is the app or device that wrote it to Apple Health (for example Oura, Eight Sleep or Alex's Apple Watch), and device is the hardware Apple Health recorded, when the writing app provides one (Apple Watch readings say Apple Watch; most other apps leave it null). Both are null on total and daily_average rows, which combine every source.
| Param | Type | Notes |
|---|---|---|
type | string | Optional. Filter by metric (e.g. heartRate). |
kind | string | Optional. reading, total or daily_average. |
source | string | Optional. Only readings whose source_app contains this text, ignoring case (e.g. Oura or Watch). |
start_date | string | Local day, YYYY-MM-DD. Optional. |
end_date | string | Local day, YYYY-MM-DD. Optional. |
limit | integer | 1–20000. Defaults to 100. |
offset | integer | Rows to skip, for paging. Defaults to 0. |
curl "https://healthapi.app/api/v1/health/samples?type=heartRate&start_date=2026-10-11&end_date=2026-10-12" \
-H "Authorization: Bearer $KEY"Workouts
Your workout sessions: type, duration, distance, and energy.
| Param | Type | Notes |
|---|---|---|
activity_type | string | Optional. Filter by workout activity type. |
start_date | string | Local day, YYYY-MM-DD. Optional. |
end_date | string | Local day, YYYY-MM-DD. Optional. |
limit | integer | 1–20000. Defaults to 100. |
offset | integer | Rows to skip, for paging. Defaults to 0. |
curl "https://healthapi.app/api/v1/health/workouts?limit=20" \
-H "Authorization: Bearer $KEY"Latest readings
The most recent reading for every metric you sync, in one indexed call, ideal for a dashboard.
curl "https://healthapi.app/api/v1/health/latest" \
-H "Authorization: Bearer $KEY"{
"heartRate": { "value": 62, "unit": "count/min", "date": "2026-10-12T14:02:00Z" },
…
}Regenerate key
Issues a new key and invalidates the current one immediately. It also disconnects every app connected through the connector, such as Claude and ChatGPT. Update your scripts and integrations that use the key afterward.
curl -X POST "https://healthapi.app/api/v1/account/regenerate-key" \
-H "Authorization: Bearer $KEY"{
"api_key": "<new key>",
"created_at": "2026-10-12T14:22:00Z",
"expires_at": null
}Export your data
Downloads a complete JSON export of your account, health samples, and workouts.
curl "https://healthapi.app/api/v1/gdpr/export" \
-H "Authorization: Bearer $KEY" \
-o export.jsonDelete account
Deactivates your account immediately and schedules permanent deletion. You have a 30-day grace period: sign back in within 30 days to recover everything. For immediate, irreversible erasure, use DELETE /gdpr/account with a confirmation body.
curl -X DELETE "https://healthapi.app/api/v1/account/delete" \
-H "Authorization: Bearer $KEY"Rate limits
Quotas are per account, per day, and the window resets at 00:00 UTC. Your own data uploads from the app don't count against them; only API queries do.
Free for 2 days, no card needed. Then an optional 7-day free trial when you subscribe. When the 2 days are up, AI apps and the API stop reading your data until you choose a plan in the app. Your synced data is kept for about 5 weeks, so subscribing picks up where you left off. There is no permanent free tier. The plans differ only in requests per day and are bought and managed in the iPhone app; see plans for details.
| Plan | Requests / day | Price | Best for |
|---|---|---|---|
| Free preview | 25 | Free for 2 days | Before you subscribe, no card needed |
| Pro | 100 | $4.99 a month or $49.99 a year | Personal dashboards |
| Developer | 1,000 | $9.99 a month or $99.99 a year | Agents that poll |
Only health reads (/health/* and /query/*) and connector tool calls count toward the daily quota, including ones rejected for bad input (400 or 422). Health read responses carry your live quota in headers, with Reset as a Unix timestamp:
X-RateLimit-Limit: 25
X-RateLimit-Remaining: 18
X-RateLimit-Reset: 1791849600Exceeding the limit returns 429 Too Many Requests with a Retry-After header. Separately, most REST routes allow 100 requests a minute per IP address; account routes (/usage, /gdpr, /account) report that limit in the same headers, with Reset in seconds.
Errors
Errors return the appropriate HTTP status with a JSON body shaped like:
{
"detail": "Rate limit exceeded. Your Pro plan allows 100 requests/day. A plan with a higher daily limit is available in the HealthAPI app."
}| Status | Meaning |
|---|---|
400 | A malformed date or week, a date in the future, or an unknown tz. |
401 | Missing or invalid API key. |
402 | No active subscription. Your free preview has ended, or your subscription has lapsed (code = subscription_required, next to detail). Your synced data is kept for about 5 weeks, and syncing and reads resume as soon as you subscribe. This is the normal end state of a preview, not an error in your integration. |
409 | Conflict, for example an Apple ID already linked to another account (detail.code = account_exists). |
422 | Validation error: a required parameter is missing or malformed. |
429 | Rate limit exceeded. Back off until Retry-After. |
