Guide

Apple Health to Notion

Keep a health log in Notion without typing it. With HealthAPI syncing your iPhone, a short script adds a row to your database every morning with yesterday's steps, sleep, resting heart rate, HRV and workouts. GitHub runs it for free on a schedule, or use n8n if you already have it.

  • Official Notion API
  • Two requests a day
  • Updated 22 October 2026

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

Notion
Health logTable
NameDateStepsSleep (h)Resting HRWorkouts
Oct 21October 21, 20268,0167.2550
Oct 20October 20, 202612,4086.9541
Oct 19October 19, 20269,1207.9550
Oct 18October 18, 202611,2847.7541
Oct 17October 17, 202610,8716.8541
GitHub Actions, every morning
Added yesterday's row

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.

PropertyTypeFilled from
NameTitleThe date as text, such as 2026-10-21
DateDateThe day
StepsNumbertotals.steps
Sleep (h)Numbersleep_hours
Resting HRNumberaverages.resting_heart_rate_bpm
Heart rateNumberaverages.heart_rate_bpm
HRV (ms)Numberaverages.hrv_ms
Active kcalNumbertotals.active_energy_kcal
Exercise (min)Numbertotals.exercise_time_min
WorkoutsNumberworkout_count
Workout minNumberworkout_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

  1. Copy your API key

    Get HealthAPI from the App Store and let it sync. Then open Connect, tap Other tools and copy your key.
  2. Make the database

    In Notion, create a database and give it the properties in the table below, with the same names and types. Name is the title property every database already has.
  3. 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.
  4. Copy the database ID

    Open the database as a full page and copy the 32-character string in its link, before any question mark.
  5. Add two files to a private GitHub repository

    Create a private repository and add the script as healthapi_to_notion.py and the workflow as .github/workflows/health-to-notion.yml. Change America/New_York to the timezone your iPhone uses, in both places in the workflow, so the days match HealthAPI's.
  6. Add three secrets

    In the repository, open Settings, Secrets and variables, Actions, and add HEALTHAPI_KEY, NOTION_TOKEN and NOTION_DATABASE_ID as repository secrets.
  7. 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.pyPython
# 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.

.github/workflows/health-to-notion.ymlYAML
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.

TerminalShell
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.py

With 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.

The HealthAPI app icon

Your Health data, in your Notion.

Get HealthAPI for iPhone, add the script, and let the rows arrive.