Marrow Health Download

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

  1. In Marrow Health, open the Export tab, then Automated, then New automation.
  2. Paste your webhook URL (use HTTPS). Add any headers your receiver checks, such as an Authorization bearer token or an n8n header-auth key.
  3. 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.
  4. Pick JSON or CSV, the date range (Since last run is the default) and how often it runs, in minutes, hours or days.
  5. 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

  1. Add a Webhook node. Set HTTP Method to POST and give it a path, such as apple-health.
  2. For a secret, set Authentication to Header Auth and add the same header name and value to the Marrow automation.
  3. Click Listen for test event, then tap Run now in Marrow within two minutes. The payload appears in the editor.
  4. 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 }}

(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

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