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 Google account, for Google Sheets and Apps Script.
Set it up in 6 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.Open Apps Script from a new sheet
Create a Google Sheet (sheets.new opens one), then choose Extensions, then Apps Script. A code editor opens in a new tab.Paste the script
Replace everything inCode.gswith the script below, give the project a name, and select Save.Save your key
Select Project Settings (the gear), scroll to Script properties and add a property namedHEALTHAPI_KEYwith your key as its value. The key then lives with the script, not in the code.Run it once
Back in the editor, pickfillPastDaysin the function menu and select Run. Google asks you to allow the script to edit this sheet and connect to an external service. Your sheet gets a Health tab with the last 20 days.Schedule it
Select Triggers (the clock), then Add Trigger. Choose the functionaddYesterday, event source Time-driven, a Day timer and a morning hour, and save. From now on a row arrives every morning.
The script
Paste all of it into Code.gs. It reads the daily summary for each day and writes one row. If a day is already in the sheet, it updates that row instead of adding a second one.
// HealthAPI to Google Sheets: one row per day in a sheet called "Health".
// Your API key goes in Project Settings > Script properties as HEALTHAPI_KEY.
const API = "https://healthapi.app/api/v1";
const SHEET_NAME = "Health";
const FILL_DAYS = 20;
// Each column: its title, and how to read it from the daily summary.
const COLUMNS = [
["Steps", (d) => d.totals.steps],
["Active energy (kcal)", (d) => round(d.totals.active_energy_kcal)],
["Exercise (min)", (d) => round(d.totals.exercise_time_min)],
["Distance (km)", (d) => round(d.totals.distance_m / 1000, 2)],
["Resting heart rate (bpm)", (d) => round(d.averages.resting_heart_rate_bpm)],
["HRV (ms)", (d) => round(d.averages.hrv_ms)],
["Sleep (h)", (d) => d.sleep_hours],
["Workouts", (d) => d.workout_count],
["Workout time (min)", (d) => d.workout_duration_minutes],
];
// The daily trigger runs this: yesterday, and the day before again in case
// your iPhone synced late.
function addYesterday() {
writeDay(daysAgo(2));
writeDay(daysAgo(1));
}
// Run once by hand to fill in the past. One request per day.
function fillPastDays() {
for (let n = FILL_DAYS; n >= 1; n--) writeDay(daysAgo(n));
}
function writeDay(date) {
const sheet = getSheet();
const d = fetchJson("/health/daily-summary?date=" + date);
const row = [date].concat(COLUMNS.map(([, read]) => blank(read(d))));
const at = findRow(sheet, date);
if (at) sheet.getRange(at, 1, 1, row.length).setValues([row]);
else sheet.appendRow(row);
}
function fetchJson(path) {
const key = PropertiesService.getScriptProperties().getProperty("HEALTHAPI_KEY");
if (!key) throw new Error("Add your API key in Project Settings > Script properties as HEALTHAPI_KEY.");
const res = UrlFetchApp.fetch(API + path, {
headers: { Authorization: "Bearer " + key },
muteHttpExceptions: true,
});
const body = JSON.parse(res.getContentText() || "{}");
if (res.getResponseCode() !== 200) {
const why = typeof body.detail === "string" ? body.detail : res.getContentText();
throw new Error("HealthAPI " + res.getResponseCode() + ": " + why);
}
return body;
}
function getSheet() {
const book = SpreadsheetApp.getActive();
let sheet = book.getSheetByName(SHEET_NAME);
if (!sheet) {
sheet = book.insertSheet(SHEET_NAME);
sheet.appendRow(["Date"].concat(COLUMNS.map(([title]) => title)));
sheet.setFrozenRows(1);
}
return sheet;
}
// The row that already holds this date, or 0.
function findRow(sheet, date) {
const last = sheet.getLastRow();
if (last < 2) return 0;
const tz = SpreadsheetApp.getActive().getSpreadsheetTimeZone();
const dates = sheet.getRange(2, 1, last - 1, 1).getValues();
for (let i = 0; i < dates.length; i++) {
const v = dates[i][0];
const s = v instanceof Date ? Utilities.formatDate(v, tz, "yyyy-MM-dd") : String(v);
if (s === date) return i + 2;
}
return 0;
}
function daysAgo(n) {
const tz = SpreadsheetApp.getActive().getSpreadsheetTimeZone();
return Utilities.formatDate(new Date(Date.now() - n * 86400000), tz, "yyyy-MM-dd");
}
function round(v, places) {
if (v === null || v === undefined) return null;
const f = Math.pow(10, places || 0);
return Math.round(v * f) / f;
}
function blank(v) {
return v === null || v === undefined ? "" : v;
}fillPastDays goes back 20 days; change FILL_DAYS to go further, at one request per day. addYesterday is the one the trigger runs.
The columns
| Column | From the daily summary | Notes |
|---|---|---|
| Steps | totals.steps | iPhone and Apple Watch counted once, so it matches the Health app. |
| Active energy | totals.active_energy_kcal | Kilocalories. |
| Exercise | totals.exercise_time_min | Apple's Exercise ring minutes. |
| Distance | totals.distance_m | Walking and running, turned into km by the script. |
| Resting heart rate | averages.resting_heart_rate_bpm | Blank on days with no reading. |
| HRV | averages.hrv_ms | SDNN in milliseconds, as Apple Watch records it. |
| Sleep | sleep_hours | Time asleep in the night that ended that morning. |
| Workouts, workout time | workout_count, workout_duration_minutes | Workouts that started that day. |
Days are in the timezone of your HealthAPI account, the same days the Health app shows. Totals are 0 and averages blank when nothing was recorded.
Charts
Select the Date column and a metric (hold Ctrl, or ⌘ on a Mac, to pick two columns), then choose Insert, then Chart. A line chart of resting heart rate or HRV over a few months shows trends that are hard to see day to day. New rows join the chart as they arrive if you select whole columns.
If something's wrong
| You see | What to do |
|---|---|
HealthAPI 401 | The key is wrong or was regenerated. Copy it again from the app and update the script property. |
HealthAPI 402 | Your free preview has ended or your plan has lapsed. Choose a plan in the HealthAPI app; your data is still there. |
HealthAPI 429 | Today's requests are used up. They reset at 00:00 UTC; the next morning's run catches up. |
| A row of zeros | Your iPhone hadn't synced that day yet. Open HealthAPI to sync; the next run rewrites the row. |
| Add your API key… | The property is missing or misspelled. It must be HEALTHAPI_KEY, in Script properties. |
Questions
Can Apple Health sync to Google Sheets automatically?
Not on its own: the Health app has no Google Sheets option, and its export is one large XML file. With HealthAPI syncing your iPhone, the script on this page adds a row to your sheet every morning, with nothing to export by hand.
How many requests does it use?
Two a day: yesterday, and the day before again in case your iPhone synced late. Filling in the past uses one request per day, so 20 days fits in the free preview's 25 a day. Google Apps Script itself is free.
Why is the sleep on a row from the night before?
A night belongs to the morning it ended, as in the Health app: the row for Tuesday holds the sleep from Monday night into Tuesday morning.
Can I add other columns?
Yes. Add a line to COLUMNS with a title and the field to read. The daily summary also has totals.basal_energy_kcal, totals.flights_climbed, averages.heart_rate_bpm, averages.spo2_pct and averages.respiratory_rate_bpm; the API reference lists them all. For a metric that isn't in it, such as weight, use the trends endpoint.
Is my key safe in the script?
It is stored in the script's properties, which anyone with edit access to the sheet can open. Keep the sheet to yourself, or share it as view only. If the key ever leaks, regenerate it in the HealthAPI app and update the property.
Can I do this without code?
Yes, with an automation tool such as n8n, Zapier or Make: a daily schedule, an HTTP request to the same endpoint with your key, and a step that adds a row. The n8n guide walks through it.
