What you need
- An iPhone on iOS 15 or later with Apple Health. Apple Watch data comes along too.
- The HealthAPI app. Free for 2 days, no card needed. Then an optional 7-day free trial when you subscribe. The preview includes 25 requests a day. 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. Pro is $4.99 a month or $49.99 a year (100 requests a day); Developer is $9.99 a month or $99.99 a year (1,000 a day). Cancel anytime in the App Store.
- A Notion account, and a GitHub account to run the script on a schedule.
The database
Create these properties. The names must match exactly, including capitals and spaces, or Notion turns the row down.
| Property | Type | Filled from |
|---|---|---|
| Name | Title | The date as text, such as 2026-10-21 |
| Date | Date | The day |
| Steps | Number | totals.steps |
| Sleep (h) | Number | sleep_hours |
| Resting HR | Number | averages.resting_heart_rate_bpm |
| Heart rate | Number | averages.heart_rate_bpm |
| HRV (ms) | Number | averages.hrv_ms |
| Active kcal | Number | totals.active_energy_kcal |
| Exercise (min) | Number | totals.exercise_time_min |
| Workouts | Number | workout_count |
| Workout min | Number | workout_duration_minutes |
Sleep is the night that ended that morning, as in the Health app. Heart rate and HRV are blank on a day without readings; totals are 0 when nothing was recorded.
Set it up in 7 steps
Copy your API key
Get HealthAPI from the App Store and let it sync. Then open Connect, tap Other tools and copy your key.Make the database
In Notion, create a database and give it the properties in the table below, with the same names and types.Nameis the title property every database already has.Create a Notion token
In Notion's Developer portal, open Personal access tokens, select New token, give it the Notion API capability and select Create token. Copy it; Notion shows it only once.Copy the database ID
Open the database as a full page and copy the 32-character string in its link, before any question mark.Add two files to a private GitHub repository
Create a private repository and add the script ashealthapi_to_notion.pyand the workflow as.github/workflows/health-to-notion.yml. ChangeAmerica/New_Yorkto the timezone your iPhone uses, in both places in the workflow, so the days match HealthAPI's.Add three secrets
In the repository, open Settings, Secrets and variables, Actions, and addHEALTHAPI_KEY,NOTION_TOKENandNOTION_DATABASE_IDas repository secrets.Run it once
On the Actions tab, choose Apple Health to Notion and Run workflow. Two rows appear in your database. From now on it runs every morning at 8:00.
A personal access token acts as you, so the database doesn't need to be shared with anything (Notion's guide). On Notion's Free plan only workspace owners can create one.
The script
Plain Python with nothing to install. For each day it reads the daily summary, looks for that day's row by its Date, and updates it or adds a new one, so running it twice never makes a duplicate.
# HealthAPI to Notion: one row per day in a Notion database.
# Set HEALTHAPI_KEY, NOTION_TOKEN, NOTION_DATABASE_ID and TZ (your timezone).
import json
import os
import urllib.error
import urllib.request
from datetime import date, timedelta
API = "https://healthapi.app/api/v1"
NOTION = "https://api.notion.com/v1"
NOTION_VERSION = "2026-03-11"
def r(value, places=1):
return None if value is None else round(value, places)
# Each Number column in your database, and how to read it from the daily summary.
COLUMNS = [
("Steps", lambda d: d["totals"]["steps"]),
("Sleep (h)", lambda d: r(d["sleep_hours"])),
("Resting HR", lambda d: r(d["averages"]["resting_heart_rate_bpm"])),
("Heart rate", lambda d: r(d["averages"]["heart_rate_bpm"])),
("HRV (ms)", lambda d: r(d["averages"]["hrv_ms"])),
("Active kcal", lambda d: r(d["totals"]["active_energy_kcal"], 0)),
("Exercise (min)", lambda d: r(d["totals"]["exercise_time_min"], 0)),
("Workouts", lambda d: d["workout_count"]),
("Workout min", lambda d: r(d["workout_duration_minutes"])),
]
def call(method, url, headers, body=None):
data = None if body is None else json.dumps(body).encode()
req = urllib.request.Request(url, data=data, method=method, headers=headers)
try:
with urllib.request.urlopen(req, timeout=60) as res:
return json.load(res)
except urllib.error.HTTPError as e:
raise SystemExit(f"{method} {url} failed with {e.code}: {e.read().decode()[:300]}")
def healthapi(path):
return call("GET", API + path, {"Authorization": "Bearer " + os.environ["HEALTHAPI_KEY"]})
def notion(method, path, body=None):
return call(method, NOTION + path, {
"Authorization": "Bearer " + os.environ["NOTION_TOKEN"],
"Notion-Version": NOTION_VERSION,
"Content-Type": "application/json",
}, body)
def write_day(source, day):
d = healthapi("/health/daily-summary?date=" + day)
props = {
"Name": {"title": [{"text": {"content": day}}]},
"Date": {"date": {"start": day}},
}
for name, read in COLUMNS:
props[name] = {"number": read(d)}
# Update the day's row if it is already there, so a rerun never duplicates.
found = notion("POST", f"/data_sources/{source}/query", {
"filter": {"property": "Date", "date": {"equals": day}},
"page_size": 1,
})["results"]
if found:
notion("PATCH", "/pages/" + found[0]["id"], {"properties": props})
print(day, "updated")
else:
notion("POST", "/pages", {
"parent": {"type": "data_source_id", "data_source_id": source},
"properties": props,
})
print(day, "added")
def main():
db = notion("GET", "/databases/" + os.environ["NOTION_DATABASE_ID"])
source = db["data_sources"][0]["id"]
# Yesterday, and the day before again in case your iPhone synced late.
# Set DAYS to fill in more, at one HealthAPI request per day.
days = int(os.environ.get("DAYS", "2"))
for n in range(days, 0, -1):
write_day(source, (date.today() - timedelta(days=n)).isoformat())
main()
The schedule
GitHub Actions runs the script at 8:00 every morning in the timezone you set, and workflow_dispatch adds the Run workflow button. Your key and token stay in encrypted repository secrets, never in the code.
name: Apple Health to Notion
on:
schedule:
- cron: "0 8 * * *"
timezone: "America/New_York"
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- run: python3 healthapi_to_notion.py
env:
HEALTHAPI_KEY: ${{ secrets.HEALTHAPI_KEY }}
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
NOTION_DATABASE_ID: ${{ secrets.NOTION_DATABASE_ID }}
TZ: America/New_York
GitHub may start a scheduled run late when it is busy (GitHub's notes). HealthAPI has no push or webhook, so a scheduled read like this is how new data reaches Notion.
Fill in the past
To add earlier days, run the script once on your computer with DAYS set, at one HealthAPI request per day. Days already in the database are updated, not added twice.
export HEALTHAPI_KEY="your HealthAPI key"
export NOTION_TOKEN="your Notion token"
export NOTION_DATABASE_ID="your database ID"
export TZ="America/New_York"
DAYS=20 python3 healthapi_to_notion.pyWith n8n instead
If you already run n8n, start from the workflow in the n8n guide, which reads yesterday's summary each morning, and add a Notion node after the HTTP Request: resource Database Page, operation Create, your database, and a property for each column, such as Steps (Number) set to {{ $json.totals.steps }} and Date (Date) set to the same expression the HTTP Request uses for its date.
n8n's Notion credential asks for an Internal Integration Secret. Create an internal connection in Notion's Developer portal, copy its secret, and add the connection to your database from its ••• menu under Connections (Notion's help). Create always adds a row, so a second run for the same day adds a second row; the script above updates instead.
Questions
Can Apple Health sync to Notion on its own?
No. The Health app has no way to send data to Notion, and Apple offers no web API for it. HealthAPI syncs your iPhone to your own account, and the script on this page copies yesterday's numbers into your database each morning.
How many requests does it use?
Two HealthAPI requests a day: yesterday, and the day before again in case your iPhone synced late. Filling in the past uses one per day, so 20 days fits in the free preview's 25 a day. Notion's API calls don't count.
Does GitHub Actions cost anything?
Not for this. The job runs for a few seconds a day, and GitHub Free includes 2,000 minutes a month for private repositories. Scheduled runs can start late when GitHub is busy, and in a public repository GitHub pauses them after 60 days without activity, one more reason to keep the repository private.
It stopped working after a few months. Why?
Notion's personal access tokens expire, after one year at most. Create a new one and replace the NOTION_TOKEN secret. If HealthAPI answers 401, regenerate or copy your key again and update HEALTHAPI_KEY.
Can I add other columns?
Yes. Add a Number property to the database and a line to COLUMNS with the same name and the field to read, such as d["totals"]["distance_m"] or d["averages"]["spo2_pct"]. Every field is in the API reference.
