Marrow Get the app

json or csv / any https url / runs with the app closed / free

Automations: your data, pushed on a schedule.

Marrow can POST a file of your health data to any URL you choose, on the cadence you choose, from the phone directly. This page is the reference: how to set one up, what each payload looks like, and how to write something that receives it.

All Marrow guides: MCP · Self-hosted mirror · Automations · Files · Support

What an automation is

A saved job in Export, Automated: one kind of data, one format, one date range, one URL, one cadence. Marrow builds the file from its verified store and POSTs it to the URL with the app closed, after every sync commit and on each background refresh, whenever the cadence has elapsed. Nothing passes through us: the phone talks straight to your endpoint.

Setting one up

  1. Export tab, Automated, New automation.
  2. Name it, paste the https URL it should POST to. Add any headers your receiver needs (an Authorization bearer token, an X-Gitlab-Token, anything).
  3. Pick the kind: Health metrics, Workouts, Strength log, or Nutrition diary. One automation carries one kind; make one per kind you want.
  4. Pick JSON or CSV, the date range, and the cadence (minutes, hours or days).
  5. Turn it on. Run now sends a first payload so you can check the receiver before waiting for the schedule.

Date ranges

Payload shapes

Health metrics (daily totals, JSON)

The same shape as Health Auto Export's, so receivers written for that app work unchanged. Values are in the stored SI unit named in units (kg, m, mL, degC, kcal, count); convert on your side if you want imperial.

{"data": {"metrics": [
  {"name": "step_count", "units": "count",
   "data": [{"date": "2026-09-11", "qty": 8746}]},
  {"name": "body_mass", "units": "kg",
   "data": [{"date": "2026-09-11", "qty": 101.3}]},
  {"name": "sleep_analysis", "units": "hr",
   "data": [{"date": "2026-09-11", "totalSleep": 7.2, "deep": 1.1, "rem": 1.6, "core": 4.3, "awake": 0.2,
             "sleepStart": "2026-09-10 23:36:00", "sleepEnd": "2026-09-11 07:02:00"}]}
]}}

Raw samples (the Raw samples grouping) send every sample with its start and end time instead of a daily total. CSV metrics come as the wide table described on the files page.

Workouts

{"data": {"workouts": [
  {"activity": "Running", "start": "2026-09-11T18:43:41Z", "end": "2026-09-11T19:13:53Z",
   "duration_min": 30.2, "kcal": 512, "distance_mi": 3.06, "avg_hr": 174, "source": "Apple Watch"}
]}}

Strength log

Every set, plus per-exercise progress and per-muscle-group volume. Weights are in the unit you chose in Settings, Units; the unit field says which.

{"data": {"unit": "lb",
  "sets": [{"time": "2026-09-11T17:02:00Z", "session": "Push day", "exercise": "Bench Press",
            "set": 1, "weight": 185, "reps": 8, "est_1rm": 229.6}],
  "exercises": [{"exercise": "Bench Press", "best_est_1rm": 229.6,
                 "sessions": [{"date": "2026-09-11T17:02:00Z", "sets": 3, "top_weight": 185, "top_reps": 8, "est_1rm": 229.6, "volume": 3885}]}],
  "muscle_groups": [{"group": "Chest", "volume": 3108.0, "sets": 3}]
}}

Nutrition diary

{"data": {"days": [
  {"day": "2026-09-11", "entries": [
    {"meal": "breakfast", "name": "Oatmeal", "grams": 250, "kcal": 300,
     "protein_g": 10, "carbs_g": 54, "fat_g": 5, "logged_at": "2026-09-11T12:10:00Z",
     "nutrients": {"dietary_fiber": 8.0, "dietary_sugar": 1.2}}
  ]}
]}}

Writing a receiver

Anything that accepts an HTTPS POST and answers 2xx works: a webhook service, a serverless function, a script on a home server. Three rules keep it robust:

A twelve-line Python receiver that archives every post:

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 (a Cloudflare Tunnel, Tailscale Funnel, or any reverse proxy) and point the automation at it. If what you want is a queryable copy rather than a folder of files, the self-hosted mirror is that receiver already written.

Good to know