Files
HealthLog/docs/api/openapi.yaml
T
Marc-André Bombeck c6d2b212f2 fix(auth): self-hoster opt-out for session-cookie Secure flag (v1.5.1)
v1.5.1 patch. Fixes the silent login failure self-hosters hit on
plain-HTTP origins that aren't localhost — the bundled Docker image
runs NODE_ENV=production, so every cookie carried Secure, and modern
browsers drop those on http://10.x.x.x style origins. The session
cookie never made it back to the browser, /api/auth/me returned 401,
and the login page reloaded.

New SESSION_COOKIE_SECURE env var with three values:
- unset (default) — flag follows NODE_ENV === "production" (unchanged)
- "false" — never set (LAN / VPN / Tailscale-only opt-out)
- "true"  — always set (pnpm dev fronted by HTTPS)

Shared helper at src/lib/auth/secure-cookie.ts replaces four scattered
literals across session, codex device-start, and withings connect.
Six unit cases pin the decision matrix.

OpenAPI spec generator now reads info.version from package.json so
the drift gate stays meaningful instead of catching every release bump.

Default behaviour unchanged for HTTPS-fronted deployments.
2026-05-26 19:16:44 +02:00

3170 lines
96 KiB
YAML

openapi: 3.1.0
info:
title: HealthLog API
version: 1.5.1
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
contact:
name: HealthLog
url: https://healthlog.dev
servers:
- url: https://healthlog.bombeck.io
description: Production
- url: http://localhost:3000
description: Local dev
tags:
- name: Auth
- name: Measurements
- name: Medications
- name: Mood
- name: Insights
- name: Notifications
- name: Devices
- name: Admin
paths:
/api/auth/login:
post:
tags:
- Auth
summary: Email-or-username login (password)
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:
required: true
content:
application/json:
schema:
type: object
properties:
email:
type: string
minLength: 1
password:
type: string
minLength: 1
required:
- email
- password
responses:
"200":
description: Login succeeded.
content:
application/json:
schema:
$ref: "#/components/schemas/LoginResponse"
"401": &a1
description: Authentication required or invalid credentials.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": &a2
description: Request validation failed.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"429": &a3
description: Rate limit exceeded.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
/api/auth/passkey/login-verify:
post:
tags:
- Auth
summary: Passkey assertion verification
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/PasskeyLoginVerifyRequest"
responses:
"200":
description: Assertion verified — session + optional bearer issued.
content:
application/json:
schema:
$ref: "#/components/schemas/PasskeyLoginVerifyResponse"
"401": *a1
"422": *a2
"429": *a3
/api/auth/refresh:
post:
tags:
- Auth
summary: Rotate refresh token (one-time use)
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:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RefreshTokenRequest"
responses:
"200":
description: Rotation succeeded — new pair issued.
content:
application/json:
schema:
$ref: "#/components/schemas/RefreshResponse"
"401": *a1
"422": *a2
"429": *a3
/api/measurements:
get:
tags:
- Measurements
summary: List measurements
description: Filter by type + date range. Response is paged via offset/limit; default limit 100, hard cap 500.
parameters:
- in: query
name: type
schema:
type: string
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
- in: query
name: from
schema:
type: string
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)))$
- in: query
name: to
schema:
type: string
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)))$
- in: query
name: limit
schema:
default: 100
type: integer
minimum: 1
maximum: 5000
- in: query
name: offset
schema:
default: 0
type: integer
minimum: 0
maximum: 9007199254740991
- in: query
name: sortBy
schema:
default: measuredAt
type: string
enum:
- type
- value
- measuredAt
- source
- in: query
name: sortDir
schema:
default: desc
type: string
enum:
- asc
- desc
- in: query
name: aggregate
schema:
type: string
enum:
- raw
- daily
- weekly
- monthly
- in: query
name: source
schema:
type: string
enum:
- rollup
- in: query
name: groupBy
schema:
type: string
enum:
- day
- in: query
name: dayKey
schema:
type: string
pattern: ^\d{4}-\d{2}-\d{2}$
responses:
"200":
description: Measurement page.
content:
application/json:
schema:
$ref: "#/components/schemas/ListMeasurementsResponse"
"401": *a1
"422": *a2
"429": *a3
post:
tags:
- Measurements
summary: Create one measurement
description: Single ingest. Use `/api/measurements/batch` for Apple Health upload streams.
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
type:
type: string
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
value:
type: number
measuredAt:
type: string
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)))$
notes:
type: string
maxLength: 25
source:
default: MANUAL
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
glucoseContext:
type: string
enum:
- FASTING
- POSTPRANDIAL
- RANDOM
- BEDTIME
deviceType:
anyOf:
- type: string
minLength: 1
maxLength: 32
- type: "null"
required:
- type
- value
- measuredAt
responses:
"201":
description: Created.
content:
application/json:
schema:
$ref: "#/components/schemas/CreateMeasurementResponse"
"401": *a1
"422": *a2
"429": *a3
/api/measurements/batch:
post:
tags:
- Measurements
summary: Apple Health batch ingest
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:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/AppleHealthBatchRequest"
responses:
"200":
description: Batch processed.
content:
application/json:
schema:
$ref: "#/components/schemas/BatchMeasurementsResponse"
"401": *a1
"422": *a2
"429": *a3
/api/measurements/by-external-ids:
delete:
tags:
- Measurements
summary: Delete measurements by external ID (iOS deletion-sync)
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:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MeasurementsDeleteByExternalIdsRequest"
responses:
"200":
description: Delete batch processed.
content:
application/json:
schema:
$ref: "#/components/schemas/MeasurementsDeleteByExternalIdsResponse"
"401": *a1
"422": *a2
"429": *a3
/api/workouts:
get:
tags:
- Measurements
summary: List workouts (v1.4.32)
description: "Paginated workout list with cross-source canonical-row dedup. The picker reads the per-user
source-priority ladder and buckets by `(startedAt 5 min slot, sportType)` — twin workouts (Apple Watch +
Withings ScanWatch) collapse to a single canonical row per cluster. Field names mirror the iOS handoff contract:
`distanceM`, `activeEnergyKcal`, `avgHr`, `maxHr`."
parameters:
- in: query
name: limit
schema:
type: integer
minimum: 1
maximum: 200
- in: query
name: offset
schema:
type: integer
minimum: 0
maximum: 9007199254740991
- in: query
name: since
schema:
type: string
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)))$
- in: query
name: until
schema:
type: string
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)))$
- in: query
name: sportType
schema:
type: string
responses:
"200":
description: Canonical workout page.
content:
application/json:
schema:
$ref: "#/components/schemas/ListWorkoutsResponse"
"401": *a1
"422": *a2
"429": *a3
/api/workouts/{id}:
get:
tags:
- Measurements
summary: Workout detail (v1.4.32)
description: Single-workout envelope. Owns the optional `WorkoutRoute` GeoJSON geometry + `canonicalId` pointer that
resolves to the cluster winner so deep-links into non-canonical twin rows can redirect cleanly. Cross-user rows
surface as 404 (existence channel sealed).
parameters:
- in: path
name: id
schema:
type: string
required: true
responses:
"200":
description: Workout detail.
content:
application/json:
schema:
$ref: "#/components/schemas/GetWorkoutResponse"
"401": *a1
"404":
description: Workout not found (or owned by another user).
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a2
"429": *a3
/api/workouts/batch:
post:
tags:
- Measurements
summary: Typed workout batch ingest (v1.4.25 W16b)
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:
required: true
content:
application/json:
schema:
type: object
properties:
workouts:
minItems: 1
maxItems: 100
type: array
items:
type: object
properties:
sportType:
type: string
enum:
- walking
- running
- cycling
- hiking
- swimming
- rowing
- elliptical
- stairClimber
- yoga
- mindAndBody
- strength
- hiit
- dance
- golf
- tennis
- basketball
- soccer
- crossTraining
- mixedCardio
- other
startedAt:
type: string
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)))$
endedAt:
type: string
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)))$
totalEnergyKcal:
type: number
minimum: 0
maximum: 50000
totalDistanceM:
type: number
minimum: 0
maximum: 1000000
avgHeartRate:
type: integer
minimum: 20
maximum: 300
maxHeartRate:
type: integer
minimum: 20
maximum: 300
minHeartRate:
type: integer
minimum: 20
maximum: 300
stepCount:
type: integer
minimum: 0
maximum: 200000
elevationM:
type: number
minimum: -500
maximum: 10000
pauseDurationSec:
type: integer
minimum: 0
maximum: 86400
source:
default: MANUAL
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
externalId:
type: string
maxLength: 128
externalSourceVersion:
type: string
maxLength: 64
metadata:
type: object
propertyNames:
type: string
additionalProperties: {}
route:
type: object
properties:
geometry:
type: object
properties:
type:
type: string
const: LineString
coordinates:
minItems: 2
maxItems: 20000
type: array
items:
type: array
prefixItems:
- type: number
minimum: -180
maximum: 180
- type: number
minimum: -90
maximum: 90
items:
type: number
required:
- type
- coordinates
sampleTimestamps:
type: array
items:
type: object
properties:
t:
type: string
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)))$
speedMs:
type: number
minimum: 0
maximum: 50
hr:
type: integer
minimum: 20
maximum: 300
required:
- t
required:
- geometry
required:
- sportType
- startedAt
- endedAt
required:
- workouts
responses:
"200":
description: Batch processed.
content:
application/json:
schema:
$ref: "#/components/schemas/BatchWorkoutsResponse"
"400":
description: Batch validation failed (over-cap, schema reject, or oversized route).
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"401": *a1
"413":
description: Request body exceeds the 5 MB ceiling.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a2
"429": *a3
/api/devices:
post:
tags:
- Devices
summary: Register native device + APNs token
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:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DeviceRegisterRequest"
responses:
"201":
description: Device registered or refreshed.
content:
application/json:
schema:
$ref: "#/components/schemas/DeviceRegisterResponse"
"401": *a1
"409":
description: Device or APNs token already registered to another user.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a2
"429": *a3
/api/auth/me/coach-prefs:
get:
tags:
- Auth
summary: Read per-user Coach prompt-tuning preferences
description: Returns the persisted preferences with defaults filled in. Null persisted state resolves to the legacy
v1.4.22 defaults.
responses:
"200":
description: Resolved preferences.
content:
application/json:
schema:
$ref: "#/components/schemas/GetCoachPrefsResponse"
"401": *a1
"422": *a2
"429": *a3
put:
tags:
- Auth
summary: Replace per-user Coach prompt-tuning preferences
description: Persists the supplied prefs. Body is validated against `CoachPrefs`; missing keys fall back to the
documented defaults.
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
tone:
default: warm
type: string
enum:
- warm
- neutral
- concise
verbosity:
default: default
type: string
enum:
- brief
- default
- detailed
excludeMetrics:
default: []
maxItems: 11
type: array
items:
type: string
enum:
- bp
- weight
- pulse
- mood
- compliance
- hrv
- sleep
- resting_hr
- steps
- medications
- anthropometrics
showEvidenceByDefault:
default: false
type: boolean
defaultWindow:
default: allTime
type: string
enum:
- last7days
- last30days
- last90days
- allTime
responses:
"200":
description: Saved preferences echoed back.
content:
application/json:
schema:
$ref: "#/components/schemas/PutCoachPrefsResponse"
"401": *a1
"422": *a2
"429": *a3
/api/auth/me/disable-coach:
get:
tags:
- Auth
summary: Read per-user Coach opt-out flag
description: Returns the user's per-account Coach opt-out flag. Default `false` (Coach visible). Powers the Settings →
Insights "Hide Coach" Switch and the layout FAB short-circuit.
responses:
"200":
description: Resolved flag.
content:
application/json:
schema:
$ref: "#/components/schemas/GetDisableCoachResponse"
"401": *a1
"422": *a2
"429": *a3
patch:
tags:
- Auth
summary: Toggle per-user Coach opt-out flag
description: Flips the per-account Coach opt-out flag. Idempotent — the DB write fires even when the value matches so
the audit-log row mirrors the API call. Rate-limit 60/min per user.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DisableCoachFlag"
responses:
"200":
description: Resolved next-state echoed back for optimistic-update consumers.
content:
application/json:
schema:
$ref: "#/components/schemas/PatchDisableCoachResponse"
"401": *a1
"422": *a2
"429": *a3
/api/auth/me/source-priority:
get:
tags:
- Auth
summary: Read per-user source priority
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":
description: Resolved priority.
content:
application/json:
schema:
$ref: "#/components/schemas/GetSourcePriorityResponse"
"401": *a1
"422": *a2
"429": *a3
put:
tags:
- Auth
summary: Replace per-user source priority
description: Persists the supplied priority. Body is validated against `SourcePriority`; missing keys read as
`DEFAULT_SOURCE_PRIORITY` at the call site.
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
steps:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
activeEnergy:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
walkingRunningDistance:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
flightsClimbed:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
sleep:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
weight:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
bloodPressure:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
pulse:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
bodyFat:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
bodyTemperature:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
spo2:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
hrv:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
restingHeartRate:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
vo2Max:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
metricPriority:
type: object
properties:
steps:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
activeEnergy:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
walkingRunningDistance:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
flightsClimbed:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
sleep:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
weight:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
bloodPressure:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
pulse:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
bodyFat:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
bodyTemperature:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
spo2:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
hrv:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
restingHeartRate:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
vo2Max:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
deviceTypePriority:
type: object
properties:
default:
maxItems: 8
type: array
items:
type: string
enum:
- watch
- band
- ring
- phone
- scale
- other
- unknown
additionalProperties:
maxItems: 8
type: array
items:
type: string
enum:
- watch
- band
- ring
- phone
- scale
- other
- unknown
responses:
"200":
description: Saved priority (defaulted) echoed back.
content:
application/json:
schema:
$ref: "#/components/schemas/PutSourcePriorityResponse"
"401": *a1
"422": *a2
"429": *a3
/api/insights/chat/messages/{id}/feedback:
post:
tags:
- Insights
summary: Rate a Coach assistant message
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:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CoachMessageFeedbackRequest"
responses:
"201":
description: Feedback saved.
content:
application/json:
schema:
$ref: "#/components/schemas/CoachMessageFeedbackResponse"
"401": *a1
"404":
description: Message not found or not owned by the caller.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"409":
description: Caller has already rated this message text.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a2
"429": *a3
/api/admin/notifications/diagnostic:
get:
tags:
- Admin
summary: Admin notification diagnostic snapshot
description: "Admin-only. Returns what the dispatcher would see when targeting the calling admin's account: registered
devices (APNs tokens masked to prefix + suffix), per-channel enabled + configPresent flags, and recent push
attempts (currently always `[]` pending the v1.4.48+ PushAttempt table). Cookie auth only — never Bearer."
responses:
"200":
description: Diagnostic snapshot.
content:
application/json:
schema:
$ref: "#/components/schemas/AdminDiagnosticResponse"
"401": *a1
"403":
description: Caller is not an admin.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a2
"429": *a3
/api/insights/comprehensive:
get:
tags:
- Insights
summary: Comprehensive AI insights bundle
description: Full Insights surface — daily briefing, recommendations with rationale, optional weekly report + storyboard
annotations. Strict-schema validated server-side.
responses:
"200":
description: Insights bundle.
content:
application/json:
schema:
$ref: "#/components/schemas/InsightsComprehensiveResponseEnvelope"
"401": *a1
"422": *a2
"429": *a3
components:
schemas:
PasskeyLoginVerifyRequest:
type: object
properties:
challengeId:
type: string
minLength: 1
description: Server-issued WebAuthn challenge id.
credential:
type: object
properties:
id:
type: string
rawId:
type: string
type:
type: string
const: public-key
response:
type: object
propertyNames:
type: string
additionalProperties: {}
authenticatorAttachment:
type: string
enum:
- platform
- cross-platform
clientExtensionResults:
type: object
propertyNames:
type: string
additionalProperties: {}
required:
- id
- rawId
- type
- response
description: SimpleWebAuthn-style assertion response payload.
required:
- challengeId
- credential
description: Passkey assertion verification. Same native-client token issuance as the password path.
RefreshTokenRequest:
type: object
properties:
refreshToken:
type: string
minLength: 1
description: Caller-presented refresh token (`hlr_<64hex>`).
revoke:
description: When true, revoke the supplied token instead of rotating.
type: boolean
required:
- refreshToken
description: Exchange a one-time-use refresh token for a fresh access + refresh pair.
AppleHealthBatchRequest:
type: object
properties:
entries:
minItems: 1
maxItems: 500
type: array
items:
$ref: "#/components/schemas/AppleHealthBatchEntry"
required:
- entries
description: Apple Health batch ingest. ≤500 entries per call; idempotent via `Idempotency-Key`.
AppleHealthBatchEntry:
type: object
properties:
hkIdentifier:
type: string
minLength: 1
maxLength: 120
description: HealthKit identifier (e.g. `HKQuantityTypeIdentifierBodyMass`).
value:
type: number
unit:
type: string
minLength: 1
maxLength: 60
startDate:
type: string
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)))$
endDate:
type: string
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)))$
sleepStage:
description: HKCategoryValueSleepAnalysis codepoint; only for sleep samples.
type: integer
minimum: 0
maximum: 20
externalId:
type: string
minLength: 1
maxLength: 120
description: HKSample.uuid string — the dedup key.
externalSourceVersion:
type: string
minLength: 1
maxLength: 120
deviceType:
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.
anyOf:
- type: string
enum:
- watch
- band
- ring
- phone
- scale
- other
- unknown
- type: "null"
required:
- hkIdentifier
- value
- unit
- startDate
- endDate
- externalId
MeasurementsDeleteByExternalIdsRequest:
type: object
properties:
externalIds:
minItems: 0
maxItems: 500
type: array
items:
type: string
minLength: 1
maxLength: 120
required:
- externalIds
description: iOS deletion-sync. Up to 500 externalIds per call; matching rows owned by other users are silently skipped
(cross-user 404 guard).
DeviceRegisterRequest:
type: object
properties:
token:
type: string
minLength: 8
maxLength: 512
pattern: ^[A-Za-z0-9+/=._:-]+$
description: Generic device identifier (legacy; APNs token below).
bundleId:
type: string
minLength: 1
maxLength: 128
locale:
type: string
minLength: 2
maxLength: 16
appVersion:
type: string
minLength: 1
maxLength: 32
model:
type: string
minLength: 1
maxLength: 64
apnsToken:
description: Hex-encoded APNs device token. Must be paired with `apnsEnvironment`.
type: string
minLength: 8
maxLength: 256
pattern: ^[A-Fa-f0-9]+$
apnsEnvironment:
description: Gateway the iOS client received `apnsToken` from. Server never auto-detects.
type: string
enum:
- sandbox
- production
required:
- token
- bundleId
description: Native device registration. Re-registering an APNs token belonging to another user returns 409
(cross-user-hijack guard).
DisableCoachFlag:
type: object
properties:
disableCoach:
type: boolean
required:
- disableCoach
description: Per-account Coach opt-out toggle (v1.4.47 W3). `true` hides the Coach FAB and short-circuits its API gates.
CoachMessageFeedbackRequest:
type: object
properties:
rating:
type: string
enum:
- helpful
- unhelpful
reason:
type: string
minLength: 1
maxLength: 200
required:
- rating
description: Per-message helpful/unhelpful feedback (v1.4.23 H7). Optional `reason` is free-form prose, capped at 200 chars.
LoginResponse:
type: object
properties:
data:
$ref: "#/components/schemas/AccessRefreshBundle"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
AccessRefreshBundle:
type: object
properties:
user:
type: object
properties:
id:
type: string
username:
type: string
required:
- id
- username
additionalProperties: false
token:
type: string
description: Access token (`hlk_<64hex>`).
tokenExpiresAt:
type: string
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)))$
refreshToken:
description: Refresh token (`hlr_<64hex>`); only present for native-policy callers.
type: string
refreshTokenExpiresAt:
type: string
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)))$
required:
- token
- tokenExpiresAt
additionalProperties: false
description: Native-client token bundle returned by login + refresh. Web cookie-only callers see only `user`.
ErrorEnvelope:
type: object
properties:
data:
type: "null"
error:
type: string
meta:
type: object
properties:
requestId:
type: string
errorCode:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
description: "Standard error response: data is null, error is human prose."
PasskeyLoginVerifyResponse:
type: object
properties:
data:
$ref: "#/components/schemas/AccessRefreshBundle"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
RefreshResponse:
type: object
properties:
data:
$ref: "#/components/schemas/AccessRefreshBundle"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
ListMeasurementsResponse:
type: object
properties:
data:
type: object
properties:
measurements:
type: array
items:
$ref: "#/components/schemas/MeasurementResource"
total:
type: integer
minimum: 0
maximum: 9007199254740991
required:
- measurements
- total
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
MeasurementResource:
type: object
properties:
id:
type: string
type:
type: string
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
value:
type: number
unit:
type: string
measuredAt:
type: string
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)))$
source:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
notes:
anyOf:
- type: string
- type: "null"
required:
- id
- type
- value
- unit
- measuredAt
- source
additionalProperties: false
description: Server-shaped measurement row returned by GET endpoints.
CreateMeasurementResponse:
type: object
properties:
data:
$ref: "#/components/schemas/MeasurementResource"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
BatchMeasurementsResponse:
type: object
properties:
data:
$ref: "#/components/schemas/AppleHealthBatchResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
AppleHealthBatchResponse:
type: object
properties:
inserted:
type: integer
minimum: 0
maximum: 9007199254740991
duplicate:
type: integer
minimum: 0
maximum: 9007199254740991
skipped:
type: integer
minimum: 0
maximum: 9007199254740991
entries:
type: array
items:
$ref: "#/components/schemas/AppleHealthBatchEntryResult"
required:
- inserted
- duplicate
- skipped
- entries
additionalProperties: false
AppleHealthBatchEntryResult:
type: object
properties:
index:
type: integer
minimum: 0
maximum: 9007199254740991
status:
type: string
enum:
- inserted
- duplicate
- skipped
reason:
type: string
required:
- index
- status
additionalProperties: false
MeasurementsDeleteByExternalIdsResponse:
type: object
properties:
data:
$ref: "#/components/schemas/MeasurementsDeleteByExternalIdsResult"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
MeasurementsDeleteByExternalIdsResult:
type: object
properties:
deletedCount:
type: integer
minimum: 0
maximum: 9007199254740991
required:
- deletedCount
additionalProperties: false
ListWorkoutsResponse:
type: object
properties:
data:
$ref: "#/components/schemas/WorkoutListResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
WorkoutListResponse:
type: object
properties:
workouts:
type: array
items:
$ref: "#/components/schemas/WorkoutListEntry"
meta:
type: object
properties:
total:
type: integer
minimum: 0
maximum: 9007199254740991
limit:
type: integer
exclusiveMinimum: 0
maximum: 9007199254740991
offset:
type: integer
minimum: 0
maximum: 9007199254740991
droppedDuplicates:
type: integer
minimum: 0
maximum: 9007199254740991
required:
- total
- limit
- offset
- droppedDuplicates
additionalProperties: false
required:
- workouts
- meta
additionalProperties: false
WorkoutListEntry:
type: object
properties:
id:
type: string
sportType:
type: string
startedAt:
type: string
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)))$
endedAt:
type: string
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)))$
durationSec:
type: integer
minimum: 0
maximum: 9007199254740991
distanceM:
anyOf:
- type: number
- type: "null"
activeEnergyKcal:
anyOf:
- type: number
- type: "null"
avgHr:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
maxHr:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
source:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
externalId:
anyOf:
- type: string
- type: "null"
required:
- id
- sportType
- startedAt
- endedAt
- durationSec
- distanceM
- activeEnergyKcal
- avgHr
- maxHr
- source
- externalId
additionalProperties: false
GetWorkoutResponse:
type: object
properties:
data:
$ref: "#/components/schemas/WorkoutDetailResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
WorkoutDetailResponse:
type: object
properties:
id:
type: string
sportType:
type: string
startedAt:
type: string
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)))$
endedAt:
type: string
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)))$
durationSec:
type: integer
minimum: 0
maximum: 9007199254740991
distanceM:
anyOf:
- type: number
- type: "null"
activeEnergyKcal:
anyOf:
- type: number
- type: "null"
avgHr:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
maxHr:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
minHr:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
stepCount:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
elevationM:
anyOf:
- type: number
- type: "null"
pauseDurationSec:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
source:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
externalId:
anyOf:
- type: string
- type: "null"
metadata:
anyOf:
- {}
- type: "null"
route:
anyOf:
- $ref: "#/components/schemas/WorkoutRouteGeometry"
- type: "null"
canonicalId:
type: string
required:
- id
- sportType
- startedAt
- endedAt
- durationSec
- distanceM
- activeEnergyKcal
- avgHr
- maxHr
- minHr
- stepCount
- elevationM
- pauseDurationSec
- source
- externalId
- metadata
- route
- canonicalId
additionalProperties: false
WorkoutRouteGeometry:
type: object
properties:
geometry: {}
sampleTimestamps:
anyOf:
- type: array
items:
type: string
- type: "null"
required:
- geometry
- sampleTimestamps
additionalProperties: false
BatchWorkoutsResponse:
type: object
properties:
data:
$ref: "#/components/schemas/WorkoutBatchResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
WorkoutBatchResponse:
type: object
properties:
processed:
type: integer
minimum: 0
maximum: 9007199254740991
inserted:
type: integer
minimum: 0
maximum: 9007199254740991
duplicates:
type: integer
minimum: 0
maximum: 9007199254740991
skipped:
type: array
items:
type: object
properties:
index:
type: integer
minimum: 0
maximum: 9007199254740991
reason:
type: string
required:
- index
- reason
additionalProperties: false
entries:
type: array
items:
$ref: "#/components/schemas/WorkoutBatchEntryResult"
required:
- processed
- inserted
- duplicates
- skipped
- entries
additionalProperties: false
WorkoutBatchEntryResult:
type: object
properties:
index:
type: integer
minimum: 0
maximum: 9007199254740991
status:
type: string
enum:
- inserted
- duplicate
- skipped
reason:
type: string
required:
- index
- status
additionalProperties: false
DeviceRegisterResponse:
type: object
properties:
data:
type: object
properties:
id:
type: string
required:
- id
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
GetCoachPrefsResponse:
type: object
properties:
data:
type: object
properties:
tone:
default: warm
type: string
enum:
- warm
- neutral
- concise
verbosity:
default: default
type: string
enum:
- brief
- default
- detailed
excludeMetrics:
default: []
maxItems: 11
type: array
items:
type: string
enum:
- bp
- weight
- pulse
- mood
- compliance
- hrv
- sleep
- resting_hr
- steps
- medications
- anthropometrics
showEvidenceByDefault:
default: false
type: boolean
defaultWindow:
default: allTime
type: string
enum:
- last7days
- last30days
- last90days
- allTime
required:
- tone
- verbosity
- excludeMetrics
- showEvidenceByDefault
- defaultWindow
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
PutCoachPrefsResponse:
type: object
properties:
data:
type: object
properties:
tone:
default: warm
type: string
enum:
- warm
- neutral
- concise
verbosity:
default: default
type: string
enum:
- brief
- default
- detailed
excludeMetrics:
default: []
maxItems: 11
type: array
items:
type: string
enum:
- bp
- weight
- pulse
- mood
- compliance
- hrv
- sleep
- resting_hr
- steps
- medications
- anthropometrics
showEvidenceByDefault:
default: false
type: boolean
defaultWindow:
default: allTime
type: string
enum:
- last7days
- last30days
- last90days
- allTime
required:
- tone
- verbosity
- excludeMetrics
- showEvidenceByDefault
- defaultWindow
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
GetDisableCoachResponse:
type: object
properties:
data:
$ref: "#/components/schemas/DisableCoachFlagOutput"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
PatchDisableCoachResponse:
type: object
properties:
data:
$ref: "#/components/schemas/DisableCoachFlagOutput"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
GetSourcePriorityResponse:
type: object
properties:
data:
type: object
properties:
steps:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
activeEnergy:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
walkingRunningDistance:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
flightsClimbed:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
sleep:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
weight:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
bloodPressure:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
pulse:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
bodyFat:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
bodyTemperature:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
spo2:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
hrv:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
restingHeartRate:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
vo2Max:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
metricPriority:
type: object
properties:
steps:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
activeEnergy:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
walkingRunningDistance:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
flightsClimbed:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
sleep:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
weight:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
bloodPressure:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
pulse:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
bodyFat:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
bodyTemperature:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
spo2:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
hrv:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
restingHeartRate:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
vo2Max:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
additionalProperties: false
deviceTypePriority:
type: object
properties:
default:
maxItems: 8
type: array
items:
type: string
enum:
- watch
- band
- ring
- phone
- scale
- other
- unknown
additionalProperties:
maxItems: 8
type: array
items:
type: string
enum:
- watch
- band
- ring
- phone
- scale
- other
- unknown
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
PutSourcePriorityResponse:
type: object
properties:
data:
type: object
properties:
steps:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
activeEnergy:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
walkingRunningDistance:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
flightsClimbed:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
sleep:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
weight:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
bloodPressure:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
pulse:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
bodyFat:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
bodyTemperature:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
spo2:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
hrv:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
restingHeartRate:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
vo2Max:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
metricPriority:
type: object
properties:
steps:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
activeEnergy:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
walkingRunningDistance:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
flightsClimbed:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
sleep:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
weight:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
bloodPressure:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
pulse:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
bodyFat:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
bodyTemperature:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
spo2:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
hrv:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
restingHeartRate:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
vo2Max:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
additionalProperties: false
deviceTypePriority:
type: object
properties:
default:
maxItems: 8
type: array
items:
type: string
enum:
- watch
- band
- ring
- phone
- scale
- other
- unknown
additionalProperties:
maxItems: 8
type: array
items:
type: string
enum:
- watch
- band
- ring
- phone
- scale
- other
- unknown
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
CoachMessageFeedbackResponse:
type: object
properties:
data:
type: object
properties:
id:
type: string
createdAt:
type: string
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)))$
required:
- id
- createdAt
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
AdminDiagnosticResponse:
type: object
properties:
data:
$ref: "#/components/schemas/AdminDiagnosticData"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
AdminDiagnosticData:
type: object
properties:
devices:
type: array
items:
$ref: "#/components/schemas/AdminDiagnosticDevice"
notificationChannels:
type: array
items:
$ref: "#/components/schemas/AdminDiagnosticChannel"
recentPushAttempts:
type: array
items:
$ref: "#/components/schemas/AdminDiagnosticPushAttempt"
required:
- devices
- notificationChannels
- recentPushAttempts
additionalProperties: false
description: Admin notification diagnostic snapshot for the calling user — what the dispatcher would see when targeting
this account. Surfaces device tokens (masked), channel state, and recent push attempts so an operator can debug
an iOS / Web Push / Telegram / ntfy issue without DB shell access.
AdminDiagnosticDevice:
type: object
properties:
id:
type: string
platform:
type: string
hasApnsToken:
type: boolean
apnsTokenPrefix:
anyOf:
- type: string
- type: "null"
apnsTokenSuffix:
anyOf:
- type: string
- type: "null"
apnsEnvironment:
anyOf:
- type: string
- type: "null"
lastSeenAt:
type: string
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)))$
required:
- id
- platform
- hasApnsToken
- apnsTokenPrefix
- apnsTokenSuffix
- apnsEnvironment
- lastSeenAt
additionalProperties: false
description: Per-device APNs registration snapshot. The APNs token is masked to its 8-char hex prefix + 8-char suffix so
an operator can correlate with iOS-side logs without disclosing the delivery target.
AdminDiagnosticChannel:
type: object
properties:
type:
type: string
enabled:
type: boolean
configPresent:
type: boolean
required:
- type
- enabled
- configPresent
additionalProperties: false
description: Per-channel state. `configPresent` is true when the channel's encrypted-JSON config blob carries the field
the corresponding sender will read (chatId+botToken for TELEGRAM, topic for NTFY); WEB_PUSH/APNS rely on sibling
tables so the row's existence is the signal.
AdminDiagnosticPushAttempt:
type: object
properties:
eventType:
type: string
channel:
type: string
result:
type: string
reason:
anyOf:
- type: string
- type: "null"
at:
type: string
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)))$
required:
- eventType
- channel
- result
- reason
- at
additionalProperties: false
description: Single push delivery attempt. Reserved for the v1.4.48+ follow-up that adds the dedicated PushAttempt table
— currently always returned as an empty array so the response shape stays stable for iOS consumers.
InsightsComprehensiveResponseEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/InsightsComprehensiveResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
InsightsComprehensiveResponse:
type: object
properties:
summary:
type: string
recommendations:
type: array
items:
type: object
propertyNames:
type: string
additionalProperties: {}
citations:
type: array
items:
type: object
propertyNames:
type: string
additionalProperties: {}
warnings:
type: array
items:
type: object
propertyNames:
type: string
additionalProperties: {}
dailyBriefing:
anyOf:
- type: object
propertyNames:
type: string
additionalProperties: {}
- type: "null"
trendAnnotations:
anyOf:
- type: object
propertyNames:
type: string
additionalProperties: {}
- type: "null"
storyboardAnnotations:
type: array
items:
type: object
propertyNames:
type: string
additionalProperties: {}
metricSource:
type: object
propertyNames:
type: string
additionalProperties: {}
required:
- summary
- recommendations
- citations
- warnings
additionalProperties: false
description: AI-generated insights bundle. Strict-schema validated server-side; Coach-routed when the insight surface
needs day-level grounding.
DisableCoachFlagOutput:
type: object
properties:
disableCoach:
type: boolean
required:
- disableCoach
additionalProperties: false
description: Per-account Coach opt-out toggle (v1.4.47 W3). `true` hides the Coach FAB and short-circuits its API gates.
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: hlk_<64hex>
description: Native-client API token. Use the `token` field returned by /api/auth/login on a native client.
cookieAuth:
type: apiKey
in: cookie
name: healthlog_session
description: Browser session cookie — set by /api/auth/login.