Send Apple Health data to a webhook
Apple Health has no web API, so your data reaches a webhook only when an app on your iPhone sends it. Marrow Health does this for free: an automation POSTs your health metrics, workouts, strength sets, food diary, or symptoms and mood to any URL, as JSON or CSV, on the schedule you pick, with the app closed. Below are the setup, the payload, and working receivers for n8n, Home Assistant and a plain server. We make Marrow.
Set up the automation
- In Marrow Health, open the Export tab, then Automated, then New automation.
- Paste your webhook URL (use HTTPS). Add any headers your receiver checks, such as an
Authorizationbearer token or an n8n header-auth key. - Pick the kind of data: Health metrics, Workouts, Strength log, Nutrition diary, or Symptoms and mood. One automation sends one kind, and you can have up to ten.
- Pick JSON or CSV, the date range (Since last run is the default) and how often it runs, in minutes, hours or days.
- Turn it on and tap Run now to send a first payload while you watch the receiver.
Each request is a POST with Content-Type: application/json (or text/csv) and your headers. Every run, with its response code, is listed in the automation's history in the app.
What arrives
Health metrics arrive as daily totals, in the unit named in units. Heart rate comes as each day's average, minimum and maximum, and sleep as each night with its stages, in hours:
{"data": {"metrics": [
{"name": "body_mass", "units": "kg",
"data": [{"date": "2026-09-11", "qty": 81.3}]},
{"name": "step_count", "units": "count",
"data": [{"date": "2026-09-11", "qty": 8746}]},
{"name": "heart_rate", "units": "bpm",
"data": [{"date": "2026-09-11", "Avg": 71, "Min": 52, "Max": 148}]},
{"name": "sleep_analysis", "units": "h",
"data": [{"date": "2026-09-11", "totalSleep": 7.2, "asleep": 7.2, "deep": 1.1, "rem": 1.6, "core": 4.3, "awake": 0.2,
"sleepStart": "2026-09-10 23:36:00 -0400", "sleepEnd": "2026-09-11 07:02:00 -0400"}]}
]}}
The workout, strength, nutrition and mood payloads, raw samples instead of daily totals, and the CSV layout are on the automations reference.
Receive it in n8n
- Add a Webhook node. Set HTTP Method to
POSTand give it a path, such asapple-health. - For a secret, set Authentication to Header Auth and add the same header name and value to the Marrow automation.
- Click Listen for test event, then tap Run now in Marrow within two minutes. The payload appears in the editor.
- Publish the workflow and point Marrow at the Production URL (the one with
/webhook/, not/webhook-test/). The test URL stops listening after a test.
The payload is under body, so today's steps are {{ $json.body.data.metrics.find(m => m.name === 'step_count').data.at(-1).qty }} in an expression. Add a Split Out node on body.data.metrics to handle each metric as its own item. n8n accepts payloads up to 16 MB by default, which covers years of daily totals. Production runs show under Executions, not in the editor. (n8n Webhook docs)
Receive it in Home Assistant
A webhook trigger takes the POST at /api/webhook/<webhook_id>. This automation stores the latest step count in a number helper called input_number.steps_today (create it first, with a maximum above your best day):
automation:
- alias: "Apple Health from Marrow"
triggers:
- trigger: webhook
webhook_id: "pick-a-long-random-id"
allowed_methods:
- POST
local_only: false
actions:
- action: input_number.set_value
target:
entity_id: input_number.steps_today
data:
value: >-
{{ ((trigger.json.data.metrics
| selectattr('name', 'eq', 'step_count') | first).data
| last).qty }}
- The JSON body is in
trigger.jsonbecause Marrow sendsContent-Type: application/json. local_onlyis on by default, which accepts posts only from your home network or a Nabu Casa cloud webhook URL. Set it tofalsewhen the phone posts over the internet to your own HTTPS address.- The webhook ID is the only secret, so make it long and random.
(Home Assistant webhook trigger docs)
Receive it on your own server
Any endpoint that accepts an HTTPS POST and answers 2xx works. This one archives every payload to a file:
from http.server import BaseHTTPRequestHandler, HTTPServer
import time
class H(BaseHTTPRequestHandler):
def do_POST(self):
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
with open(f"marrow-{int(time.time())}.json", "wb") as f:
f.write(body)
self.send_response(200)
self.end_headers()
HTTPServer(("0.0.0.0", 8798), H).serve_forever()
Put it behind HTTPS with a Cloudflare Tunnel, Tailscale Funnel or a reverse proxy. If you would rather have a queryable database than a folder of files, Marrow's self-hosted mirror is that receiver already written, and AI agents can query it too.
Three rules for a receiver that never loses data
- Latest wins. Store each value keyed by metric and day and overwrite on repeat. Since last run re-sends the last two days on purpose, because Apple Health keeps filling in a day after it ends.
- Save the raw body first, then parse it. A parsing bug should never cost you data.
- Answer quickly. Marrow waits up to the timeout you set (60 seconds by default) and retries a failed run from the last successful window on its next turn.
When does it run?
Whenever iOS lets Marrow run: after it syncs with Apple Health, when you open it, and during background refresh, once the interval you chose has passed. iOS decides when background refresh happens, so a phone left locked all day sends when it is next unlocked, and Since last run covers the gap.
Other ways to do it
- Health Auto Export also posts Apple Health data to webhooks, and its metrics JSON has the same structure as Marrow's. Its automations need the Premium tier.
- Apple Shortcuts can read health samples and post them with Get Contents of URL, on a personal automation at a set time. It is free, it can only read Health data while the phone is unlocked, and you build and maintain each metric yourself.
- Apple's export.zip is a one-off full backup, not a feed. To turn one into CSV, use our in-browser converter.