components: schemas: AccessRefreshBundle: additionalProperties: false description: Native-client token bundle returned by login + refresh. Web cookie-only callers see only `user`. properties: refreshToken: description: Refresh token (`hlr_<64hex>`); only present for native-policy callers. type: string refreshTokenExpiresAt: format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ type: string token: description: Access token (`hlk_<64hex>`). type: string tokenExpiresAt: format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ type: string user: additionalProperties: false properties: id: type: string username: type: string required: - id - username type: object required: - token - tokenExpiresAt type: object AppleHealthBatchEntry: properties: deviceType: anyOf: - enum: - watch - band - ring - phone - scale - other - unknown type: string - type: "null" description: Device class mapped from `HKDevice.model`. Used by the analytics aggregator to break ties when the same source contributed multiple devices for the same day. Omit (or send null) on legacy clients — the server treats it as `unknown` and the picker falls through. endDate: format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ type: string externalId: description: HKSample.uuid string — the dedup key. maxLength: 120 minLength: 1 type: string externalSourceVersion: maxLength: 120 minLength: 1 type: string hkIdentifier: description: HealthKit identifier (e.g. `HKQuantityTypeIdentifierBodyMass`). maxLength: 120 minLength: 1 type: string sleepStage: description: HKCategoryValueSleepAnalysis codepoint; only for sleep samples. maximum: 20 minimum: 0 type: integer startDate: format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ type: string unit: maxLength: 60 minLength: 1 type: string value: type: number required: - hkIdentifier - value - unit - startDate - endDate - externalId type: object AppleHealthBatchEntryResult: additionalProperties: false properties: index: maximum: 9007199254740991 minimum: 0 type: integer reason: type: string status: enum: - inserted - duplicate - skipped type: string required: - index - status type: object AppleHealthBatchRequest: description: Apple Health batch ingest. ≤500 entries per call; idempotent via `Idempotency-Key`. properties: entries: items: $ref: "#/components/schemas/AppleHealthBatchEntry" maxItems: 500 minItems: 1 type: array required: - entries type: object AppleHealthBatchResponse: additionalProperties: false properties: duplicate: maximum: 9007199254740991 minimum: 0 type: integer entries: items: $ref: "#/components/schemas/AppleHealthBatchEntryResult" type: array inserted: maximum: 9007199254740991 minimum: 0 type: integer skipped: maximum: 9007199254740991 minimum: 0 type: integer required: - inserted - duplicate - skipped - entries type: object BatchMeasurementsResponse: additionalProperties: false properties: data: $ref: "#/components/schemas/AppleHealthBatchResponse" error: type: "null" meta: additionalProperties: false properties: requestId: type: string type: object required: - data - error type: object BatchWorkoutsResponse: additionalProperties: false properties: data: $ref: "#/components/schemas/WorkoutBatchResponse" error: type: "null" meta: additionalProperties: false properties: requestId: type: string type: object required: - data - error type: object CoachMessageFeedbackRequest: description: Per-message helpful/unhelpful feedback (v1.4.23 H7). Optional `reason` is free-form prose, capped at 200 chars. properties: rating: enum: - helpful - unhelpful type: string reason: maxLength: 200 minLength: 1 type: string required: - rating type: object CoachMessageFeedbackResponse: additionalProperties: false properties: data: additionalProperties: false properties: createdAt: format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ type: string id: type: string required: - id - createdAt type: object error: type: "null" meta: additionalProperties: false properties: requestId: type: string type: object required: - data - error type: object CreateMeasurementResponse: additionalProperties: false properties: data: $ref: "#/components/schemas/MeasurementResource" error: type: "null" meta: additionalProperties: false properties: requestId: type: string type: object required: - data - error type: object DeviceRegisterRequest: description: Native device registration. Re-registering an APNs token belonging to another user returns 409 (cross-user-hijack guard). properties: apnsEnvironment: description: Gateway the iOS client received `apnsToken` from. Server never auto-detects. enum: - sandbox - production type: string apnsToken: description: Hex-encoded APNs device token. Must be paired with `apnsEnvironment`. maxLength: 256 minLength: 8 pattern: ^[A-Fa-f0-9]+$ type: string appVersion: maxLength: 32 minLength: 1 type: string bundleId: maxLength: 128 minLength: 1 type: string locale: maxLength: 16 minLength: 2 type: string model: maxLength: 64 minLength: 1 type: string token: description: Generic device identifier (legacy; APNs token below). maxLength: 512 minLength: 8 pattern: ^[A-Za-z0-9+/=._:-]+$ type: string required: - token - bundleId type: object DeviceRegisterResponse: additionalProperties: false properties: data: additionalProperties: false properties: id: type: string required: - id type: object error: type: "null" meta: additionalProperties: false properties: requestId: type: string type: object required: - data - error type: object ErrorEnvelope: additionalProperties: false description: "Standard error response: data is null, error is human prose." properties: data: type: "null" error: type: string meta: additionalProperties: false properties: errorCode: type: string requestId: type: string type: object required: - data - error type: object GetCoachPrefsResponse: additionalProperties: false properties: data: additionalProperties: false properties: defaultWindow: default: allTime enum: - last7days - last30days - last90days - allTime type: string excludeMetrics: default: [] items: enum: - bp - weight - pulse - mood - compliance - hrv - sleep - resting_hr - steps type: string maxItems: 9 type: array showEvidenceByDefault: default: false type: boolean tone: default: warm enum: - warm - neutral - concise type: string verbosity: default: default enum: - brief - default - detailed type: string required: - tone - verbosity - excludeMetrics - showEvidenceByDefault - defaultWindow type: object error: type: "null" meta: additionalProperties: false properties: requestId: type: string type: object required: - data - error type: object GetSourcePriorityResponse: additionalProperties: false properties: data: additionalProperties: false properties: activeEnergy: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array bloodPressure: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array bodyFat: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array bodyTemperature: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array deviceTypePriority: additionalProperties: items: enum: - watch - band - ring - phone - scale - other - unknown type: string maxItems: 8 type: array properties: default: items: enum: - watch - band - ring - phone - scale - other - unknown type: string maxItems: 8 type: array type: object flightsClimbed: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array hrv: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array metricPriority: additionalProperties: false properties: activeEnergy: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array bloodPressure: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array bodyFat: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array bodyTemperature: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array flightsClimbed: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array hrv: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array pulse: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array restingHeartRate: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array sleep: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array spo2: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array steps: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array vo2Max: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array walkingRunningDistance: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array weight: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array type: object pulse: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array restingHeartRate: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array sleep: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array spo2: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array steps: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array vo2Max: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array walkingRunningDistance: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array weight: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array type: object error: type: "null" meta: additionalProperties: false properties: requestId: type: string type: object required: - data - error type: object InsightsComprehensiveResponse: additionalProperties: false description: AI-generated insights bundle. Strict-schema validated server-side; Coach-routed when the insight surface needs day-level grounding. properties: citations: items: additionalProperties: {} propertyNames: type: string type: object type: array dailyBriefing: anyOf: - additionalProperties: {} propertyNames: type: string type: object - type: "null" metricSource: additionalProperties: {} propertyNames: type: string type: object recommendations: items: additionalProperties: {} propertyNames: type: string type: object type: array storyboardAnnotations: items: additionalProperties: {} propertyNames: type: string type: object type: array summary: type: string trendAnnotations: anyOf: - additionalProperties: {} propertyNames: type: string type: object - type: "null" warnings: items: additionalProperties: {} propertyNames: type: string type: object type: array required: - summary - recommendations - citations - warnings type: object InsightsComprehensiveResponseEnvelope: additionalProperties: false properties: data: $ref: "#/components/schemas/InsightsComprehensiveResponse" error: type: "null" meta: additionalProperties: false properties: requestId: type: string type: object required: - data - error type: object ListMeasurementsResponse: additionalProperties: false properties: data: additionalProperties: false properties: measurements: items: $ref: "#/components/schemas/MeasurementResource" type: array total: maximum: 9007199254740991 minimum: 0 type: integer required: - measurements - total type: object error: type: "null" meta: additionalProperties: false properties: requestId: type: string type: object required: - data - error type: object LoginResponse: additionalProperties: false properties: data: $ref: "#/components/schemas/AccessRefreshBundle" error: type: "null" meta: additionalProperties: false properties: requestId: type: string type: object required: - data - error type: object MeasurementResource: additionalProperties: false description: Server-shaped measurement row returned by GET endpoints. properties: id: type: string measuredAt: format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ type: string notes: anyOf: - type: string - type: "null" source: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string type: enum: - WEIGHT - BLOOD_PRESSURE_SYS - BLOOD_PRESSURE_DIA - PULSE - BODY_FAT - SLEEP_DURATION - ACTIVITY_STEPS - BLOOD_GLUCOSE - TOTAL_BODY_WATER - BONE_MASS - OXYGEN_SATURATION - HEART_RATE_VARIABILITY - RESTING_HEART_RATE - ACTIVE_ENERGY_BURNED - FLIGHTS_CLIMBED - WALKING_RUNNING_DISTANCE - VO2_MAX - BODY_TEMPERATURE - FAT_FREE_MASS - FAT_MASS - MUSCLE_MASS - SKIN_TEMPERATURE - PULSE_WAVE_VELOCITY - VASCULAR_AGE - VISCERAL_FAT - AUDIO_EXPOSURE_ENV - AUDIO_EXPOSURE_HEADPHONE - TIME_IN_DAYLIGHT - WALKING_STEADINESS - AUDIO_EXPOSURE_EVENT type: string unit: type: string value: type: number required: - id - type - value - unit - measuredAt - source type: object MeasurementsDeleteByExternalIdsRequest: description: iOS deletion-sync. Up to 500 externalIds per call; matching rows owned by other users are silently skipped (cross-user 404 guard). properties: externalIds: items: maxLength: 120 minLength: 1 type: string maxItems: 500 minItems: 0 type: array required: - externalIds type: object MeasurementsDeleteByExternalIdsResponse: additionalProperties: false properties: data: $ref: "#/components/schemas/MeasurementsDeleteByExternalIdsResult" error: type: "null" meta: additionalProperties: false properties: requestId: type: string type: object required: - data - error type: object MeasurementsDeleteByExternalIdsResult: additionalProperties: false properties: deletedCount: maximum: 9007199254740991 minimum: 0 type: integer required: - deletedCount type: object PasskeyLoginVerifyRequest: description: Passkey assertion verification. Same native-client token issuance as the password path. properties: challengeId: description: Server-issued WebAuthn challenge id. minLength: 1 type: string credential: description: SimpleWebAuthn-style assertion response payload. properties: authenticatorAttachment: enum: - platform - cross-platform type: string clientExtensionResults: additionalProperties: {} propertyNames: type: string type: object id: type: string rawId: type: string response: additionalProperties: {} propertyNames: type: string type: object type: const: public-key type: string required: - id - rawId - type - response type: object required: - challengeId - credential type: object PasskeyLoginVerifyResponse: additionalProperties: false properties: data: $ref: "#/components/schemas/AccessRefreshBundle" error: type: "null" meta: additionalProperties: false properties: requestId: type: string type: object required: - data - error type: object PutCoachPrefsResponse: additionalProperties: false properties: data: additionalProperties: false properties: defaultWindow: default: allTime enum: - last7days - last30days - last90days - allTime type: string excludeMetrics: default: [] items: enum: - bp - weight - pulse - mood - compliance - hrv - sleep - resting_hr - steps type: string maxItems: 9 type: array showEvidenceByDefault: default: false type: boolean tone: default: warm enum: - warm - neutral - concise type: string verbosity: default: default enum: - brief - default - detailed type: string required: - tone - verbosity - excludeMetrics - showEvidenceByDefault - defaultWindow type: object error: type: "null" meta: additionalProperties: false properties: requestId: type: string type: object required: - data - error type: object PutSourcePriorityResponse: additionalProperties: false properties: data: additionalProperties: false properties: activeEnergy: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array bloodPressure: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array bodyFat: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array bodyTemperature: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array deviceTypePriority: additionalProperties: items: enum: - watch - band - ring - phone - scale - other - unknown type: string maxItems: 8 type: array properties: default: items: enum: - watch - band - ring - phone - scale - other - unknown type: string maxItems: 8 type: array type: object flightsClimbed: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array hrv: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array metricPriority: additionalProperties: false properties: activeEnergy: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array bloodPressure: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array bodyFat: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array bodyTemperature: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array flightsClimbed: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array hrv: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array pulse: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array restingHeartRate: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array sleep: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array spo2: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array steps: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array vo2Max: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array walkingRunningDistance: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array weight: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array type: object pulse: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array restingHeartRate: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array sleep: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array spo2: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array steps: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array vo2Max: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array walkingRunningDistance: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array weight: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array type: object error: type: "null" meta: additionalProperties: false properties: requestId: type: string type: object required: - data - error type: object RefreshResponse: additionalProperties: false properties: data: $ref: "#/components/schemas/AccessRefreshBundle" error: type: "null" meta: additionalProperties: false properties: requestId: type: string type: object required: - data - error type: object RefreshTokenRequest: description: Exchange a one-time-use refresh token for a fresh access + refresh pair. properties: refreshToken: description: Caller-presented refresh token (`hlr_<64hex>`). minLength: 1 type: string revoke: description: When true, revoke the supplied token instead of rotating. type: boolean required: - refreshToken type: object WorkoutBatchEntryResult: additionalProperties: false properties: index: maximum: 9007199254740991 minimum: 0 type: integer reason: type: string status: enum: - inserted - duplicate - skipped type: string required: - index - status type: object WorkoutBatchResponse: additionalProperties: false properties: duplicates: maximum: 9007199254740991 minimum: 0 type: integer entries: items: $ref: "#/components/schemas/WorkoutBatchEntryResult" type: array inserted: maximum: 9007199254740991 minimum: 0 type: integer processed: maximum: 9007199254740991 minimum: 0 type: integer skipped: items: additionalProperties: false properties: index: maximum: 9007199254740991 minimum: 0 type: integer reason: type: string required: - index - reason type: object type: array required: - processed - inserted - duplicates - skipped - entries type: object securitySchemes: bearerAuth: bearerFormat: hlk_<64hex> description: Native-client API token. Use the `token` field returned by /api/auth/login on a native client. scheme: bearer type: http cookieAuth: description: Browser session cookie — set by /api/auth/login. in: cookie name: healthlog_session type: apiKey info: contact: name: HealthLog url: https://healthlog.dev description: >- Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest. ## Date-time contract Every field marked `format: date-time` is an ISO-8601 instant. The schema pattern accepts both `Z` (UTC) and `±HH:MM` (offset) suffixes; clients must accept either. Server responses since v1.4.25 emit timestamps with the requesting user's display-timezone offset when a user context exists (exports, doctor-report PDF). Background-job-authored payloads (backups, admin tools) emit `Z`. Native iOS clients should always parse via a Foundation ISO-8601 formatter with the `withInternetDateTime` + `withTimeZone` options enabled. license: name: AGPL-3.0 url: https://github.com/MBombeck/HealthLog/blob/main/LICENSE title: HealthLog API version: 1.4.23 openapi: 3.1.0 paths: /api/auth/login: post: description: "Browser callers receive a session cookie. Native callers (X-Client-Type: native or HealthLog-iOS UA prefix) additionally receive a paired access + refresh token." requestBody: content: application/json: schema: properties: email: minLength: 1 type: string password: minLength: 1 type: string required: - email - password type: object required: true responses: "200": content: application/json: schema: $ref: "#/components/schemas/LoginResponse" description: Login succeeded. "401": &a1 content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" description: Authentication required or invalid credentials. "422": &a2 content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" description: Request validation failed. "429": &a3 content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" description: Rate limit exceeded. summary: Email-or-username login (password) tags: - Auth /api/auth/me/coach-prefs: get: description: Returns the persisted preferences with defaults filled in. Null persisted state resolves to the legacy v1.4.22 defaults. responses: "200": content: application/json: schema: $ref: "#/components/schemas/GetCoachPrefsResponse" description: Resolved preferences. "401": *a1 "422": *a2 "429": *a3 summary: Read per-user Coach prompt-tuning preferences tags: - Auth put: description: Persists the supplied prefs. Body is validated against `CoachPrefs`; missing keys fall back to the documented defaults. requestBody: content: application/json: schema: properties: defaultWindow: default: allTime enum: - last7days - last30days - last90days - allTime type: string excludeMetrics: default: [] items: enum: - bp - weight - pulse - mood - compliance - hrv - sleep - resting_hr - steps type: string maxItems: 9 type: array showEvidenceByDefault: default: false type: boolean tone: default: warm enum: - warm - neutral - concise type: string verbosity: default: default enum: - brief - default - detailed type: string type: object required: true responses: "200": content: application/json: schema: $ref: "#/components/schemas/PutCoachPrefsResponse" description: Saved preferences echoed back. "401": *a1 "422": *a2 "429": *a3 summary: Replace per-user Coach prompt-tuning preferences tags: - Auth /api/auth/me/source-priority: get: description: Returns the per-metric-class source priority used by the analytics aggregator. Missing keys fall back to `DEFAULT_SOURCE_PRIORITY`. Null persisted state resolves to the documented defaults. responses: "200": content: application/json: schema: $ref: "#/components/schemas/GetSourcePriorityResponse" description: Resolved priority. "401": *a1 "422": *a2 "429": *a3 summary: Read per-user source priority tags: - Auth put: description: Persists the supplied priority. Body is validated against `SourcePriority`; missing keys read as `DEFAULT_SOURCE_PRIORITY` at the call site. requestBody: content: application/json: schema: properties: activeEnergy: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array bloodPressure: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array bodyFat: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array bodyTemperature: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array deviceTypePriority: additionalProperties: items: enum: - watch - band - ring - phone - scale - other - unknown type: string maxItems: 8 type: array properties: default: items: enum: - watch - band - ring - phone - scale - other - unknown type: string maxItems: 8 type: array type: object flightsClimbed: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array hrv: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array metricPriority: properties: activeEnergy: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array bloodPressure: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array bodyFat: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array bodyTemperature: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array flightsClimbed: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array hrv: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array pulse: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array restingHeartRate: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array sleep: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array spo2: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array steps: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array vo2Max: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array walkingRunningDistance: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array weight: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array type: object pulse: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array restingHeartRate: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array sleep: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array spo2: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array steps: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array vo2Max: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array walkingRunningDistance: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array weight: items: enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string maxItems: 8 type: array type: object required: true responses: "200": content: application/json: schema: $ref: "#/components/schemas/PutSourcePriorityResponse" description: Saved priority (defaulted) echoed back. "401": *a1 "422": *a2 "429": *a3 summary: Replace per-user source priority tags: - Auth /api/auth/passkey/login-verify: post: requestBody: content: application/json: schema: $ref: "#/components/schemas/PasskeyLoginVerifyRequest" required: true responses: "200": content: application/json: schema: $ref: "#/components/schemas/PasskeyLoginVerifyResponse" description: Assertion verified — session + optional bearer issued. "401": *a1 "422": *a2 "429": *a3 summary: Passkey assertion verification tags: - Auth /api/auth/refresh: post: description: Reuse of a consumed refresh token revokes every refresh token still active for the originating device (per-device blast radius from v1.4.23). Legacy tokens issued before v1.4.23 with a null deviceId fall back to revoke-all-for-user. requestBody: content: application/json: schema: $ref: "#/components/schemas/RefreshTokenRequest" required: true responses: "200": content: application/json: schema: $ref: "#/components/schemas/RefreshResponse" description: Rotation succeeded — new pair issued. "401": *a1 "422": *a2 "429": *a3 summary: Rotate refresh token (one-time use) tags: - Auth /api/devices: post: description: Idempotent upsert by `token`. APNs token + environment are paired — supplying one without the other returns 422. Cross-user re-registration of either identifier returns 409. requestBody: content: application/json: schema: $ref: "#/components/schemas/DeviceRegisterRequest" required: true responses: "201": content: application/json: schema: $ref: "#/components/schemas/DeviceRegisterResponse" description: Device registered or refreshed. "401": *a1 "409": content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" description: Device or APNs token already registered to another user. "422": *a2 "429": *a3 summary: Register native device + APNs token tags: - Devices /api/insights/chat/messages/{id}/feedback: post: description: Persists a helpful/unhelpful rating for a single Coach reply. Reuses the v1.4.16 RecommendationFeedback table via the polymorphic `targetType` column. The aggregator buckets ratings by (promptVersion, tone, verbosity). requestBody: content: application/json: schema: $ref: "#/components/schemas/CoachMessageFeedbackRequest" required: true responses: "201": content: application/json: schema: $ref: "#/components/schemas/CoachMessageFeedbackResponse" description: Feedback saved. "401": *a1 "404": content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" description: Message not found or not owned by the caller. "409": content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" description: Caller has already rated this message text. "422": *a2 "429": *a3 summary: Rate a Coach assistant message tags: - Insights /api/insights/comprehensive: get: description: Full Insights surface — daily briefing, recommendations with rationale, optional weekly report + storyboard annotations. Strict-schema validated server-side. responses: "200": content: application/json: schema: $ref: "#/components/schemas/InsightsComprehensiveResponseEnvelope" description: Insights bundle. "401": *a1 "422": *a2 "429": *a3 summary: Comprehensive AI insights bundle tags: - Insights /api/measurements: get: description: Filter by type + date range. Response is paged via offset/limit; default limit 100, hard cap 500. parameters: - in: query name: type schema: enum: - WEIGHT - BLOOD_PRESSURE_SYS - BLOOD_PRESSURE_DIA - PULSE - BODY_FAT - SLEEP_DURATION - ACTIVITY_STEPS - BLOOD_GLUCOSE - TOTAL_BODY_WATER - BONE_MASS - OXYGEN_SATURATION - HEART_RATE_VARIABILITY - RESTING_HEART_RATE - ACTIVE_ENERGY_BURNED - FLIGHTS_CLIMBED - WALKING_RUNNING_DISTANCE - VO2_MAX - BODY_TEMPERATURE - FAT_FREE_MASS - FAT_MASS - MUSCLE_MASS - SKIN_TEMPERATURE - PULSE_WAVE_VELOCITY - VASCULAR_AGE - VISCERAL_FAT - AUDIO_EXPOSURE_ENV - AUDIO_EXPOSURE_HEADPHONE - TIME_IN_DAYLIGHT - WALKING_STEADINESS - AUDIO_EXPOSURE_EVENT type: string - in: query name: from schema: format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ type: string - in: query name: to schema: format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ type: string - in: query name: limit schema: default: 100 maximum: 5000 minimum: 1 type: integer - in: query name: offset schema: default: 0 maximum: 9007199254740991 minimum: 0 type: integer - in: query name: sortBy schema: default: measuredAt enum: - type - value - measuredAt - source type: string - in: query name: sortDir schema: default: desc enum: - asc - desc type: string - in: query name: aggregate schema: enum: - raw - daily - weekly - monthly type: string responses: "200": content: application/json: schema: $ref: "#/components/schemas/ListMeasurementsResponse" description: Measurement page. "401": *a1 "422": *a2 "429": *a3 summary: List measurements tags: - Measurements post: description: Single ingest. Use `/api/measurements/batch` for Apple Health upload streams. requestBody: content: application/json: schema: properties: deviceType: anyOf: - maxLength: 32 minLength: 1 type: string - type: "null" glucoseContext: enum: - FASTING - POSTPRANDIAL - RANDOM - BEDTIME type: string measuredAt: format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ type: string notes: maxLength: 25 type: string source: default: MANUAL enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string type: enum: - WEIGHT - BLOOD_PRESSURE_SYS - BLOOD_PRESSURE_DIA - PULSE - BODY_FAT - SLEEP_DURATION - ACTIVITY_STEPS - BLOOD_GLUCOSE - TOTAL_BODY_WATER - BONE_MASS - OXYGEN_SATURATION - HEART_RATE_VARIABILITY - RESTING_HEART_RATE - ACTIVE_ENERGY_BURNED - FLIGHTS_CLIMBED - WALKING_RUNNING_DISTANCE - VO2_MAX - BODY_TEMPERATURE - FAT_FREE_MASS - FAT_MASS - MUSCLE_MASS - SKIN_TEMPERATURE - PULSE_WAVE_VELOCITY - VASCULAR_AGE - VISCERAL_FAT - AUDIO_EXPOSURE_ENV - AUDIO_EXPOSURE_HEADPHONE - TIME_IN_DAYLIGHT - WALKING_STEADINESS - AUDIO_EXPOSURE_EVENT type: string value: type: number required: - type - value - measuredAt type: object required: true responses: "201": content: application/json: schema: $ref: "#/components/schemas/CreateMeasurementResponse" description: Created. "401": *a1 "422": *a2 "429": *a3 summary: Create one measurement tags: - Measurements /api/measurements/batch: post: description: Up to 500 HealthKit entries per call. Idempotent via the `Idempotency-Key` header (replay window 24h). Per-entry status lets the iOS client advance its sync cursor accurately. v1.4.25 W8c adds an optional `deviceType` per entry — feed it from `HKDevice.model` so the cross-source canonical picker can break Apple-Watch-vs-iPhone ties; null/absent stays backward-compatible with v1.4.23 clients. requestBody: content: application/json: schema: $ref: "#/components/schemas/AppleHealthBatchRequest" required: true responses: "200": content: application/json: schema: $ref: "#/components/schemas/BatchMeasurementsResponse" description: Batch processed. "401": *a1 "422": *a2 "429": *a3 summary: Apple Health batch ingest tags: - Measurements /api/measurements/by-external-ids: delete: description: Removes the user's measurement rows whose externalId is in the request list. Rows owned by another user are silently skipped (cross-user 404 guard). Up to 500 externalIds per call. requestBody: content: application/json: schema: $ref: "#/components/schemas/MeasurementsDeleteByExternalIdsRequest" required: true responses: "200": content: application/json: schema: $ref: "#/components/schemas/MeasurementsDeleteByExternalIdsResponse" description: Delete batch processed. "401": *a1 "422": *a2 "429": *a3 summary: Delete measurements by external ID (iOS deletion-sync) tags: - Measurements /api/workouts/batch: post: description: Server-side ingest endpoint for HKWorkout records (iOS) and Withings activity rows (server-to-server). Up to 100 workouts per call; nested route geometry (GeoJSON LineString) is capped at 20 000 points and stored in a 1:1 `WorkoutRoute` row keyed by `workoutId`. Idempotent via the `Idempotency-Key` header (replay window 24h). Per-entry status (`inserted | duplicate | skipped`) lets the iOS sync cursor checkpoint accurately. The request body ceiling is 5 MB enforced at the HTTP layer — clients above the ceiling receive a 413 with `workout.batch.payload_too_large`. requestBody: content: application/json: schema: properties: workouts: items: properties: avgHeartRate: maximum: 300 minimum: 20 type: integer elevationM: maximum: 10000 minimum: -500 type: number endedAt: format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ type: string externalId: maxLength: 128 type: string externalSourceVersion: maxLength: 64 type: string maxHeartRate: maximum: 300 minimum: 20 type: integer metadata: additionalProperties: {} propertyNames: type: string type: object minHeartRate: maximum: 300 minimum: 20 type: integer pauseDurationSec: maximum: 86400 minimum: 0 type: integer route: properties: geometry: properties: coordinates: items: items: type: number prefixItems: - maximum: 180 minimum: -180 type: number - maximum: 90 minimum: -90 type: number type: array maxItems: 20000 minItems: 2 type: array type: const: LineString type: string required: - type - coordinates type: object sampleTimestamps: items: properties: hr: maximum: 300 minimum: 20 type: integer speedMs: maximum: 50 minimum: 0 type: number t: format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ type: string required: - t type: object type: array required: - geometry type: object source: default: MANUAL enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH type: string sportType: enum: - walking - running - cycling - hiking - swimming - rowing - elliptical - stairClimber - yoga - mindAndBody - strength - hiit - dance - golf - tennis - basketball - soccer - crossTraining - mixedCardio - other type: string startedAt: format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ type: string stepCount: maximum: 200000 minimum: 0 type: integer totalDistanceM: maximum: 1000000 minimum: 0 type: number totalEnergyKcal: maximum: 50000 minimum: 0 type: number required: - sportType - startedAt - endedAt type: object maxItems: 100 minItems: 1 type: array required: - workouts type: object required: true responses: "200": content: application/json: schema: $ref: "#/components/schemas/BatchWorkoutsResponse" description: Batch processed. "400": content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" description: Batch validation failed (over-cap, schema reject, or oversized route). "401": *a1 "413": content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" description: Request body exceeds the 5 MB ceiling. "422": *a2 "429": *a3 summary: Typed workout batch ingest (v1.4.25 W16b) tags: - Measurements servers: - description: Production url: https://healthlog.bombeck.io - description: Local dev url: http://localhost:3000 tags: - name: Auth - name: Measurements - name: Medications - name: Mood - name: Insights - name: Notifications - name: Devices - name: Admin