Format-Referenz
Diese Seite beschreibt, was die App an ein von dir eingetragenes Ziel schickt — vollständig genug, um einen Empfänger zu bauen, ohne vorher zu raten. Alle Beispiele stammen wörtlich aus den Tests der App; wenn sich das Format ändert, brechen dort Tests, bevor diese Seite veraltet.
Suchst du stattdessen den Dateiexport (CSV, JSON, NDJSON zum Herunterladen), steht der weiter unten unter Dateiexport. Wie du ein Ziel überhaupt einträgst, steht in Ein Ziel einrichten.
Der HTTP-Kontrakt
Ein Sync ist ein POST mit einem JSON-Körper. Kein Chunking, keine Query-Parameter, kein Multipart, keine Kompression.
POST https://dein-server.at/health
Content-Type: application/json
X-Sender: export-health-data
{ "data": { "metrics": [ … ], "workouts": [] } }
Nur https://. Unverschlüsselte Adressen nimmt die App gar nicht erst an.
X-Sender ist immer export-health-data und ändert sich über Versionen hinweg nicht. Beliefern mehrere Programme dieselbe Adresse, erkennst du uns daran.
Dein Zugangsschlüssel — falls du einen einträgst — wird als frei benannter Header gesetzt, und zwar zuletzt. Nennst du dein Feld ebenfalls X-Sender, gewinnt deins.
Zugestellt heißt 2xx. Jeder andere Statuscode gilt als Fehlversuch — auch 3xx, denn die App bewertet den Code, den sie am Ende sieht. Antworte mit 200, sobald du den Körper sicher hast; auf den Inhalt deiner Antwort kommt es nicht an. Die ersten 200 Zeichen davon landen im Protokoll der App, was sich für eine kurze Quittung anbietet.
Zeitbudget 90 Sekunden. Danach bricht die App ab und wertet den Versuch als „keine Antwort". Schreib also erst weg, antworte dann — und nicht umgekehrt.
Wiederholungen: bis zu sechs Versuche, mit Grundabständen von 60, 300, 900, 3600 und 21 600 Sekunden, jeweils zufällig auf die Hälfte bis das Ganze gestreut. Ein Retry-After wird nicht ausgewertet.
Größe: im Betrieb 2 bis 3 kB je Sendung.
Der Umschlag
Immer dieselben drei Schlüssel. workouts ist stets ein leeres Array — die App sendet keine Workouts, das Feld existiert nur der Kompatibilität wegen.
{ "data": { "metrics": [ … ], "workouts": [] } }
Auch ein leerer Lauf geht hinaus. Er ist kein Fehler, sondern ein Lebenszeichen:
{"data":{"metrics":[],"workouts":[]}}
Jedes Element in metrics hat genau drei Schlüssel in fester Reihenfolge:
{ "name": "…", "units": "…", "data": [ … ] }
Die Reihenfolge der Metriken ist bestimmbar: zuerst die Mengenmetriken alphabetisch nach name, danach heart_rate, zuletzt sleep_analysis. Innerhalb einer Metrik stehen die Punkte nach Tag aufsteigend.
Die zwölf Familien
Der automatische Versand trägt zwölf Familien — nicht die 188 Datentypen des Dateiexports. Achtung: drei Namen auf der Leitung weichen von den naheliegenden ab. Verlass dich auf diese Spalte, nicht auf die Apple-Bezeichnung.
name units step_count count resting_heart_rate bpm heart_rate_variability ms ← nicht …_sdnn vo2_max ml/kg/min weight_body_mass kg ← nicht body_mass body_fat_percentage % lean_body_mass kg respiratory_rate brpm blood_oxygen_saturation % ← nicht oxygen_saturation apple_sleeping_wrist_temperature celsius heart_rate bpm sleep_analysis hr
Eine Familie erscheint nur, wenn es für sie Werte gibt. Rechne nicht damit, dass alle zwölf in jeder Sendung stehen.
Die drei Punktformen
Ein Datenpunkt sieht je nach Familie unterschiedlich aus. Wer nur die erste Form implementiert, verliert Puls und Schlaf.
1 · Mengenmetrik
Neun der zehn Mengenmetriken, ein Punkt je Tag.
{"date":"2026-08-02 12:00:00 +0200","qty":97.4,"source":"healthpipe"}
2 · heart_rate
Kein qty, dafür drei großgeschriebene Felder — Tages-Minimum, -Mittel und -Maximum.
{"date":"2026-08-02 12:00:00 +0200","Min":44,"Avg":61.5,"Max":142,"source":"healthpipe"}
3 · sleep_analysis
Eine Nacht je lokalem Tag. date ist hier nicht der Mittagsstempel, sondern identisch mit sleepEnd — die Nacht gehört dem Tag, an dem sie endete.
{"date":"2026-08-02 06:40:00 +0200",
"sleepStart":"2026-08-01 23:10:00 +0200",
"sleepEnd":"2026-08-02 06:40:00 +0200",
"asleep":7.1667,"totalSleep":7.1667,
"deep":0.8333,"rem":2,"core":4.3333,"awake":0.3333,
"source":"Apple Watch von Harald"}
Fehlende Felder sind nicht null. Hat die gewählte Quelle keine echten Schlafstadien — etwa die Bewegungserkennung des iPhones —, fehlen deep, rem und core vollständig. awake erscheint nur, wenn es größer als null ist. Wer fehlende Felder als 0 einträgt, erfindet Daten.
asleep und totalSleep tragen immer denselben Wert. Nimm eines von beiden, nicht zwei Zeitreihen.
Zeit, Zahlen, Einheiten
Das Zeitformat ist kein ISO-8601. Der Offset steht ohne Doppelpunkt:
yyyy-MM-dd HH:mm:ss ±HHMM 2026-08-02 12:00:00 +0200
Strenge ISO-Parser scheitern daran. In PHP passt 'Y-m-d H:i:s O', in Python '%Y-%m-%d %H:%M:%S %z'.
Tagespunkte tragen 12:00 Uhr Ortszeit, nicht Mitternacht. Das ist Absicht: Wer den Kalendertag aus dem Zeitstempel ableitet, kann ihn an einer Zeitumstellung nicht verlieren — mittags sind es zu beiden Tagesgrenzen zwölf Stunden. Der Offset selbst wird ebenfalls zur Mittagszeit des jeweiligen Tages bestimmt, Tage beiderseits einer Umstellung tragen also ihren eigenen.
Zahlen haben höchstens vier Nachkommastellen, nachlaufende Nullen werden abgeschnitten. Deshalb steht im Beispiel oben "rem":2 und nicht 2.0000. Behandle jedes Zahlenfeld als Fließkommazahl.
Prozentwerte reisen bereits als 0–100 mit der Einheit % — 97.4 bedeutet 97,4 %. Multipliziere nicht noch einmal.
Schlafwerte reisen in Stunden (Einheit hr), nicht in Sekunden oder Minuten.
Das Feld source trägt bei Mengenmetriken und heart_rate konstant healthpipe — ein eingefrorener Bezeichner aus dem ursprünglichen Format, kein Gerätename. Nur bei Schlafpunkten steht dort der echte Name des Geräts, das die Nacht geschrieben hat, und der kann von Nacht zu Nacht wechseln. Rechne damit, dass dieses Feld dort personenbezogen ist.
Was dein Empfänger können muss
Zustellung ist at-least-once, nicht exactly-once. Zwei Gründe: Das Lesefenster ist heilend — die App schickt die letzten drei Tage mit, damit Lücken sich von selbst füllen —, und ein Versuch, dessen Antwort verlorengeht, wird wiederholt. Derselbe Tag kommt also mehrfach, mit gleichem oder korrigiertem Wert.
Speichere deshalb nicht anhängend, sondern aktualisierend: ein Schlüssel aus Metrikname, Tag und gegebenenfalls Statistik-Art, und beim zweiten Mal überschreiben. Wer stumpf einfügt, hat nach einer Woche jeden Tag drei- bis viermal.
Unveränderte Sendungen bleiben aus. Ist der Inhalt zeichengleich mit dem zuletzt bestätigten, spart die App sich den Versand — außer du tippst „Jetzt senden", dann geht er immer hinaus. Bleibt es still, ist das also kein Fehler.
Antworte schnell und schreib vorher weg. 90 Sekunden sind die Grenze; danach kommt dieselbe Sendung später noch einmal.
Dateiexport
Der Weg über den Tab „Export" erzeugt eine Datei statt einer Sendung — andere Regeln, anderes Format.
CSV (Tab „Export", Knopf „Tabelle erstellen"): vier Spalten, Tag;Metrik;Wert;Einheit, eine Zeile je Tag und Metrik. UTF-8 mit BOM, Zeilenenden CRLF. Trennzeichen und Dezimalzeichen hängen am Regionalformat — EU heißt Semikolon und Komma, US heißt Komma und Punkt. Details und die Excel-Fallen stehen in Tabelle in Excel & Numbers öffnen.
JSON und NDJSON (nur über „Erweiterter Export"): Rohmessungen statt Tageswerte, alle 188 Datentypen wählbar. Sie ignorieren das Regionalformat vollständig — immer Punkt als Dezimalzeichen, immer ISO-Zeitstempel mit Doppelpunkt im Offset. Achtung: Prozentwerte stehen dort als Bruch (0.974, Einheit pct), nicht als 97,4 wie im Versand. JSON ist ein Dokument mit {"schema":"healthpipe.samples.v1", …}, NDJSON ein Objekt je Zeile ohne BOM.
Was diese Seite nicht verspricht
Das Versandformat ist eingefroren, weil bestehende Empfänger es lesen — es wird sich nicht ohne Not ändern. Eine formale Zusicherung ist das trotzdem nicht: Kommt eine Familie dazu, steht sie irgendwann in dieser Tabelle. Bau deinen Empfänger so, dass ein unbekannter name ihn nicht umbringt, sondern übersprungen wird.
Und: Ob dein Ziel die Daten nach dem 2xx wirklich gespeichert hat, sieht die App nicht. „Zugestellt" heißt ausschließlich, dass dein Server den Empfang bestätigt hat.