Files
HealthLog/docs/api/openapi.yaml
T

6085 lines
208 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
openapi: 3.1.0
info:
title: HealthLog API
version: 1.7.2
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: Dashboard
- name: Notifications
- name: Devices
- name: Export
- name: Sync
- name: Admin
paths:
/api/export/health-record:
post:
tags:
- Export
summary: Generate a health-record export (PDF / FHIR / package)
description: "v1.7.0 flagship export. Returns the doctor-handover artefact in the requested `format`: `pdf` →
application/pdf, `fhir` → application/fhir+json (HL7 FHIR R4 document Bundle), `package` → application/zip (PDF
+ FHIR + README). Auth via cookie or Bearer; shared `export:<userId>` rate bucket (10/h). Strict validation:
unknown keys 422."
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
format:
type: string
enum:
- pdf
- fhir
- package
range:
type: object
properties:
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)))$
days:
type: integer
minimum: 1
maximum: 365
additionalProperties: false
sections:
type: object
properties:
vitals:
type: object
properties:
weight:
type: boolean
bp:
type: boolean
pulse:
type: boolean
oxygenSaturation:
type: boolean
bodyFat:
type: boolean
bodyComposition:
type: boolean
cardioFitness:
type: object
properties:
restingHeartRate:
type: boolean
hrv:
type: boolean
vo2max:
type: boolean
activity:
type: object
properties:
steps:
type: boolean
distance:
type: boolean
energy:
type: boolean
sleep:
type: boolean
glucose:
type: boolean
medications:
type: object
properties:
list:
type: boolean
compliance:
type: boolean
glp1:
type: boolean
sideEffects:
type: boolean
mood:
type: boolean
bmi:
type: boolean
locale:
type: string
enum:
- de
- en
- fr
- es
- it
- pl
practiceName:
type: string
maxLength: 120
includeCharts:
type: boolean
includeAiSummary:
type: boolean
required:
- format
additionalProperties: false
responses:
"200":
description: "Export generated. Content-Type varies by `format`: application/pdf, application/fhir+json, or
application/zip."
content:
application/pdf:
schema:
type: string
format: binary
application/fhir+json:
schema:
type: string
format: binary
application/zip:
schema:
type: string
format: binary
"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/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
"422": *a2
"429": *a3
/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.
On a 401, `meta.errorCode` is a stable machine code so the client can branch terminal re-auth from a transient
blip without parsing the prose `error`: `auth.refresh.reuse` (a consumed token was replayed — device family
revoked, re-pair required), `auth.refresh.revoked` (family revoked out-of-band — re-pair required),
`auth.refresh.invalid` (not found / expired — drop the token and re-authenticate).
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/sync/state:
get:
tags:
- Sync
summary: Sync handshake + window metadata
description: Cheap 'should I sync?' summary. Returns the previous `lastSyncedAt` checkpoint and advances it server-side
on each call. The `sync` block carries the incremental-delta window + tombstone retention so the client reads
them rather than hardcoding.
responses:
"200":
description: Sync state summary.
content:
application/json:
schema:
$ref: "#/components/schemas/SyncStateEnvelope"
"401": *a1
"422": *a2
"429": *a3
/api/sync/changes:
get:
tags:
- Sync
summary: Measurements delta feed
description: "Incremental catch-up after the first-pair backfill (never a replacement for it). Pages over an opaque
keyset cursor; each page carries `tombstones` (soft-deleted rows, keyed on `externalId`) and `upserts` (live
rows). Apply tombstones before upserts within a page. `cursorExpired: true` forces a clean re-init."
parameters:
- in: query
name: cursor
schema:
description: Opaque multi-domain keyset cursor from the previous page. Treat as fully opaque — echo, never parse. Omit
for the initial sync.
type: string
minLength: 1
maxLength: 2048
description: Opaque multi-domain keyset cursor from the previous page. Treat as fully opaque — echo, never parse. Omit
for the initial sync.
- in: query
name: limit
schema:
description: Page size, default 200, hard cap 500.
type: integer
minimum: 1
maximum: 500
description: Page size, default 200, hard cap 500.
responses:
"200":
description: Delta page.
content:
application/json:
schema:
$ref: "#/components/schemas/SyncChangesEnvelope"
"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
- RESPIRATORY_RATE
- BODY_MASS_INDEX
- LEAN_BODY_MASS
- WALKING_HEART_RATE_AVERAGE
- WALKING_ASYMMETRY
- WALKING_DOUBLE_SUPPORT
- WALKING_STEP_LENGTH
- WALKING_SPEED
- 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
- RESPIRATORY_RATE
- BODY_MASS_INDEX
- LEAN_BODY_MASS
- WALKING_HEART_RATE_AVERAGE
- WALKING_ASYMMETRY
- WALKING_DOUBLE_SUPPORT
- WALKING_STEP_LENGTH
- WALKING_SPEED
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: 200
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/medications:
get:
tags:
- Medications
summary: List medications for the calling user
description: Returns every medication owned by the caller (active + paused), ordered by `createdAt DESC`. Each row
carries its nested `schedules`, the joined clinical `category`, the latest non-skipped `lastTakenAt`, and the
count of intake events scheduled for today (`todayEventCount`). The response is cached server-side for 60 s per
user; writes flush the cache.
responses:
"200":
description: Medication list.
content:
application/json:
schema:
$ref: "#/components/schemas/ListMedicationsResponse"
"401": *a1
"422": *a2
"429": *a3
post:
tags:
- Medications
summary: Create a medication with at least one schedule
description: Validates the body against `CreateMedicationRequest`, applies the v1.5 cross-field invariants (one-shot
consistency, recurring default `FREQ=DAILY`, `timesOfDay` dual-write), and creates the medication + its
schedules in a single Prisma write. Audits as `medication.create`.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateMedicationRequest"
responses:
"201":
description: Created medication with its schedules.
content:
application/json:
schema:
$ref: "#/components/schemas/CreateMedicationResponse"
"401": *a1
"422": *a2
"429": *a3
/api/medications/{id}:
get:
tags:
- Medications
summary: Fetch a single medication
description: Returns the medication + its schedules + the joined `category`. Cross-user rows surface as 404 (existence
channel sealed).
parameters:
- in: path
name: id
schema:
type: string
required: true
responses:
"200":
description: Medication detail.
content:
application/json:
schema:
$ref: "#/components/schemas/GetMedicationResponse"
"401": *a1
"404":
description: Medication not found (or owned by another user).
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a2
"429": *a3
put:
tags:
- Medications
summary: Replace a medication (partial fields)
description: Every field on the body is optional; omitted fields are left untouched. Supplying `schedules` REPLACES the
medication's full schedule list (the route deletes existing rows before re-creating). Flipping `active` to false
stamps `pausedAt`; flipping back to true clears it. v1.5 invariants on the `schedules` array match `POST
/api/medications`. Audits as `medication.update`.
parameters:
- in: path
name: id
schema:
type: string
required: true
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateMedicationRequest"
responses:
"200":
description: Updated medication.
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateMedicationResponse"
"401": *a1
"404":
description: Medication not found (or owned by another user).
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a2
"429": *a3
delete:
tags:
- Medications
summary: Delete a medication
description: Cascades to the medication's schedules, intake events, dose changes, inventory rows, and side-effect logs.
Revokes every API token scoped to `medication:<id>:ingest`. Audits as `medication.delete`.
parameters:
- in: path
name: id
schema:
type: string
required: true
responses:
"200":
description: Deletion succeeded.
content:
application/json:
schema:
$ref: "#/components/schemas/DeleteMedicationResponse"
"401": *a1
"404":
description: Medication not found (or owned by another user).
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a2
"429": *a3
/api/medications/{id}/intake:
post:
tags:
- Medications
summary: Log an intake event for a medication
description: Records a taken or skipped dose. Idempotent via the `Idempotency-Key` header AND the optional
`idempotencyKey` body field (the route walks both paths); a re-post inside the 60 s server-side dedup window
returns the original event. Non-skipped intakes auto-decrement pen inventory (best-effort), refresh the per-day
compliance rollup, and — for `oneShot:true` medications — flip `active` to false.
parameters:
- in: path
name: id
schema:
type: string
required: true
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MedicationIntakeRequest"
responses:
"200":
description: Idempotent replay — the original event is returned without creating a new row.
content:
application/json:
schema:
$ref: "#/components/schemas/ReplayMedicationIntakeResponse"
"201":
description: Intake event created.
content:
application/json:
schema:
$ref: "#/components/schemas/CreateMedicationIntakeResponse"
"401": *a1
"404":
description: Medication not found (or owned by another user).
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a2
"429": *a3
/api/medications/extract:
post:
tags:
- Medications
summary: Extract scheduling fields from a free-text medication description
description: Runs the user's free-text description through the Coach provider chain and returns a citation-guarded
partial payload the wizard merges onto whatever the user already typed. `name` and `dose` are dropped when not
substring-matched in the original text so the wizard cannot land a hallucinated brand or dose. `cadenceKind` /
`doseUnit` / `weekdays` are closed enums; numeric fields are clamped. Rate-limited 10 requests / 5 minutes /
user, gated against the daily Coach token budget.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MedicationExtractRequest"
responses:
"200":
description: Citation-guarded partial extraction.
content:
application/json:
schema:
$ref: "#/components/schemas/MedicationExtractResponse"
"401": *a1
"422": *a2
"429": *a3
"502":
description: Upstream provider returned an empty, unparseable, or off-schema reply.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"503":
description: No AI provider configured for the calling user (or operator).
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
/api/medications/{id}/cadence:
get:
tags:
- Medications
summary: Cadence + compliance read for a medication
description: Returns the expected-vs-actual dose timeline for the requested window plus the four compliance chip values
that drive the detail-page section. Pure computation — no writes. Day boundaries are resolved in the user's IANA
timezone so a Tokyo user and a Berlin user see the same chips for the same medication. The `days` query
parameter caps at 180.
parameters:
- in: path
name: id
schema:
type: string
required: true
- in: query
name: days
schema:
description: Window size in days (default 30, max 180).
type: integer
minimum: 1
maximum: 180
description: Window size in days (default 30, max 180).
responses:
"200":
description: Cadence response.
content:
application/json:
schema:
$ref: "#/components/schemas/GetMedicationCadenceResponse"
"401": *a1
"404":
description: Medication not found (or owned by 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
dataClusters:
maxItems: 10
type: array
items:
type: string
enum:
- cardio
- body
- activity
- workouts
- sleep
- mood
- glucose
- medication
- mobility
- environment
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/user/avatar:
post:
tags:
- Auth
summary: Upload the calling user's avatar
description: "Multipart upload (field `file`). Accepts image/jpeg, image/png, image/webp; rejects anything else at the
magic-byte sniff (the multipart Content-Type header is informational only). Hard caps: 2 MiB body, 2048×2048
dimensions. Replaces the v1.4.22 Gravatar leak by storing the bytes on the User row + serving them from
same-origin."
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
description: Binary image payload (JPEG / PNG / WebP). Hard-capped at 2 MiB and 2048×2048.
required:
- file
responses:
"201":
description: Avatar saved. `avatarUrl` is the same value the /me payload returns; the `?v=` suffix busts client caches
on re-upload.
content:
application/json:
schema:
$ref: "#/components/schemas/UploadAvatarResponse"
"401": *a1
"413":
description: Upload exceeds the 2 MiB byte limit or the 2048×2048 dimension limit.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"415":
description: Image format is not one of JPEG / PNG / WebP (magic-byte sniff).
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a2
"429": *a3
delete:
tags:
- Auth
summary: Clear the calling user's avatar
description: Resets the avatar columns to null. Idempotent — a delete on an already-empty row returns 204 with no
audit-log row.
responses:
"204":
description: Avatar cleared (or already empty).
"401": *a1
"422": *a2
"429": *a3
/api/user/avatar/{id}:
get:
tags:
- Auth
summary: Serve a user's avatar bytes
description: Owner-scoped. The `{id}` path segment must match the calling user's id — cross-user reads return 403.
Response body is the raw image bytes with the persisted `Content-Type`; the /me payload appends
`?v={updatedAtMs}` so clients can cache aggressively.
parameters:
- in: path
name: id
schema:
type: string
description: User id (must match the calling user).
required: true
description: User id (must match the calling user).
responses:
"200":
description: Avatar bytes.
content:
image/jpeg:
schema:
type: string
description: Raw JPEG bytes.
image/png:
schema:
type: string
description: Raw PNG bytes.
image/webp:
schema:
type: string
description: Raw WebP bytes.
"401": *a1
"403":
description: Caller is not the avatar's owner.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"404":
description: The user has no uploaded avatar.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a2
"429": *a3
/api/insights/layout:
get:
tags:
- Insights
summary: Read the calling user's Insights tile layout
description: Returns the per-user Insights tile layout (visibility + order). Falls back to the default layout when the
user has not customised it. Mirrors the dashboard-widgets contract.
responses:
"200":
description: The resolved layout (custom or default).
content:
application/json:
schema:
$ref: "#/components/schemas/InsightsLayout"
"401": *a1
"422": *a2
"429": *a3
put:
tags:
- Insights
summary: Replace the calling user's Insights tile layout
description: Persists the full tile layout. The normalised layout is returned. Invalid bodies return the multi-issue 422
envelope, matching the dashboard-widgets route.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/InsightsLayoutBody"
responses:
"200":
description: Layout saved; the normalised layout is echoed back.
content:
application/json:
schema:
$ref: "#/components/schemas/InsightsLayoutSaved"
"401": *a1
"422": *a2
"429": *a3
delete:
tags:
- Insights
summary: Reset the calling user's Insights tile layout
description: Clears the persisted layout and returns the default layout. Idempotent.
responses:
"200":
description: Layout reset; the default layout is returned.
content:
application/json:
schema:
$ref: "#/components/schemas/InsightsLayoutReset"
"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/dashboard/snapshot:
get:
tags:
- Dashboard
summary: Unified dashboard first-paint snapshot
description: "Assembles every above-the-fold tile field in one round-trip from the rollup / mood / widget helpers plus a
read-only lift of the pre-generated daily briefing. Two-phase: `tiles` always present, `extras` nullable on a
rollup-coverage miss. No LLM is reachable from this path. Cookie or Bearer auth."
responses:
"200":
description: Dashboard snapshot.
content:
application/json:
schema:
$ref: "#/components/schemas/DashboardSnapshotResponseEnvelope"
"401": *a1
"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
medicationDelivery:
description: v1.7.0 per-device medication-delivery override. NULL / omitted = inherit the user-level roaming default.
"server" forces server APNs for this device; "client" forces local. Stored + echoed; cron suppression stays
user-level.
anyOf:
- type: string
enum:
- server
- client
- type: "null"
required:
- token
- bundleId
description: Native device registration. Re-registering an APNs token belonging to another user returns 409
(cross-user-hijack guard).
CreateMedicationRequest:
type: object
properties:
name:
type: string
minLength: 1
maxLength: 100
dose:
type: string
minLength: 1
maxLength: 50
category:
type: string
enum:
- BLOOD_PRESSURE
- VITAMIN
- SUPPLEMENT
- PAIN_RELIEF
- ALLERGY
- DIGESTIVE
- THYROID
- HORMONE
- SKIN
- SLEEP_AID
- DIABETES
- ANTIBIOTIC
- OTHER
treatmentClass:
type: string
enum:
- GENERIC
- GLP1
dosesPerUnit:
type: integer
minimum: 1
maximum: 100
deliveryForm:
type: string
enum:
- ORAL
- INJECTION
- OTHER
notificationsEnabled:
type: boolean
liveActivityEnabled:
description: iOS Live Activity opt-in for this medication's reminders. Default false. The iOS client owns the
ActivityKit lifecycle; the server only stores + echoes the flag.
type: boolean
criticalAlarmEnabled:
description: iOS 26 AlarmKit critical-reminder opt-in. Default false. Critical alarms bypass the device mute switch /
Focus; the server stores the preference only and hangs no server-side behaviour off it.
type: boolean
startsOn:
description: Date the medication course begins (ISO `YYYY-MM-DD`). Anchors RRULE BYDAY / BYMONTHDAY patterns and the
rolling-interval countdown's first window. NULL = active from creation (the legacy implicit behaviour).
Required when `oneShot` is true.
anyOf:
- type: string
format: date
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])))$
- type: "null"
endsOn:
description: Date the medication course ends (ISO `YYYY-MM-DD`). NULL = no end date (chronic). When `oneShot` is true
the route normalises `endsOn` to equal `startsOn`.
anyOf:
- type: string
format: date
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])))$
- type: "null"
oneShot:
description: Single-administration medication (e.g. flu shot, post-op single dose). When true the medication has at most
one schedule with no `rrule` / `rollingIntervalDays`, and `active` auto-flips to false once the dose is
logged (non-skipped).
type: boolean
schedules:
minItems: 1
type: array
items:
$ref: "#/components/schemas/MedicationScheduleInput"
required:
- name
- dose
- schedules
description: 'Create-medication body. The route enforces the v1.5 cross-field invariants on top of the per-schedule
`rrule_xor_rolling` Zod refine: a `oneShot:true` medication may carry at most one schedule and that schedule
must not declare a recurrence; `endsOn` is normalised to equal `startsOn` for one-shot doses; a recurring
schedule with no `rrule`, `rollingIntervalDays`, or legacy `daysOfWeek` defaults to `rrule = "FREQ=DAILY"`; and
`timesOfDay` is dual-written from `windowStart` when the caller omits it.'
MedicationScheduleInput:
type: object
properties:
windowStart:
type: string
pattern: ^([01]\d|2[0-3]):([0-5]\d)$
description: Legacy single-time-of-intake (HH:mm, user local). v1.5 keeps the field for backwards compatibility with
pre-wizard iOS clients; the new `timesOfDay` array supersedes it.
windowEnd:
type: string
pattern: ^([01]\d|2[0-3]):([0-5]\d)$
description: Legacy reminder-window upper bound (HH:mm). Used to derive the late-classification grace span when
`reminderGraceMinutes` is null.
label:
description: Optional human label (e.g. "Morning", "Evening").
type: string
maxLength: 50
dose:
description: Per-schedule dose override. NULL means the schedule inherits `Medication.dose`.
type: string
maxLength: 50
daysOfWeek:
description: Legacy day-of-week filter (0=Sunday..6=Saturday). v1.5 reads new writes through `rrule` first; this field
is preserved for pre-v1.5 rows and is the input the route serialises into the persisted `days_of_week`
string.
type: array
items:
type: integer
minimum: 0
maximum: 6
intervalWeeks:
description: Legacy multi-week stride (1..4). Bi-weekly + tri-weekly were broken in the pre-v1.5 reminder worker; new
writes encode the same intent via `rrule` (e.g. `FREQ=WEEKLY;INTERVAL=2;BYDAY=WE`).
type: integer
minimum: 1
maximum: 4
timesOfDay:
description: v1.5 first-class points-in-time the dose is taken (HH:mm, user local). Up to 8 entries. Absent or empty
means the route stamps `[windowStart]` so the new engine always sees a populated array.
maxItems: 8
type: array
items:
type: string
pattern: ^([01]\d|2[0-3]):([0-5]\d)$
reminderGraceMinutes:
description: Reminder grace window in minutes. Caps at 24h. NULL falls back to the legacy `windowEnd - windowStart` span.
type: integer
minimum: 1
maximum: 1440
rrule:
description: "RFC 5545 RRULE string (subset). Use for daily / weekly-with-BYDAY / multi-week / monthly / yearly
cadences. Mutually exclusive with `rollingIntervalDays`. Examples: `FREQ=DAILY`,
`FREQ=WEEKLY;BYDAY=MO,WE,FR`, `FREQ=WEEKLY;INTERVAL=2;BYDAY=WE`, `FREQ=MONTHLY;BYMONTHDAY=1`,
`FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=1`."
type: string
maxLength: 200
pattern: ^FREQ=(?:DAILY|WEEKLY|MONTHLY|YEARLY)(?:;(?:INTERVAL=\d{1,3}|BYDAY=(?:MO|TU|WE|TH|FR|SA|SU)(?:,(?:MO|TU|WE|TH|FR|SA|SU))*|BYMONTHDAY=-?\d{1,2}(?:,-?\d{1,2})*|BYMONTH=\d{1,2}(?:,\d{1,2})*|COUNT=\d{1,4}|UNTIL=\d{8}T\d{6}Z))*$
rollingIntervalDays:
description: Flexible-rolling interval in days, counted forward from the latest `MedicationIntakeEvent.takenAt` (the
dose re-anchors when logged). Mutually exclusive with `rrule`. Range 1..365.
type: integer
minimum: 1
maximum: 365
scheduleType:
description: Schedule type. SCHEDULED (default) = rrule / rolling / legacy cadence. PRN = as-needed (never projected,
reminded, or counted in compliance expected; still loggable via the intake route). CYCLIC = N weeks on / M
weeks off, gating whichever inner cadence the rrule / legacy fields describe.
type: string
enum:
- SCHEDULED
- PRN
- CYCLIC
cyclicOnWeeks:
description: Cyclic "on" weeks (1..52). Required when `scheduleType` is CYCLIC; ignored otherwise.
type: integer
minimum: 1
maximum: 52
cyclicOffWeeks:
description: Cyclic "off" weeks (0..52). Required when `scheduleType` is CYCLIC; ignored otherwise.
type: integer
minimum: 0
maximum: 52
required:
- windowStart
- windowEnd
description: Single schedule entry on a medication. v1.5 introduces `timesOfDay`, `rrule`, `rollingIntervalDays`, and
`reminderGraceMinutes` as first-class fields; `windowStart`, `windowEnd`, `daysOfWeek`, and `intervalWeeks` are
preserved through the v1.5.x line for backwards compatibility. **`rrule` and `rollingIntervalDays` are mutually
exclusive** — supplying both fails 422 (`rrule_xor_rolling`). The DB enforces the same invariant via a CHECK
constraint.
UpdateMedicationRequest:
type: object
properties:
name:
type: string
minLength: 1
maxLength: 100
dose:
type: string
minLength: 1
maxLength: 50
category:
type: string
enum:
- BLOOD_PRESSURE
- VITAMIN
- SUPPLEMENT
- PAIN_RELIEF
- ALLERGY
- DIGESTIVE
- THYROID
- HORMONE
- SKIN
- SLEEP_AID
- DIABETES
- ANTIBIOTIC
- OTHER
treatmentClass:
type: string
enum:
- GENERIC
- GLP1
dosesPerUnit:
anyOf:
- type: integer
minimum: 1
maximum: 100
- type: "null"
deliveryForm:
type: string
enum:
- ORAL
- INJECTION
- OTHER
active:
type: boolean
notificationsEnabled:
type: boolean
liveActivityEnabled:
description: iOS Live Activity opt-in for this medication's reminders. The iOS client owns the ActivityKit lifecycle;
the server only stores + echoes the flag.
type: boolean
criticalAlarmEnabled:
description: iOS 26 AlarmKit critical-reminder opt-in. Critical alarms bypass the device mute switch / Focus; the server
stores the preference only.
type: boolean
startsOn:
description: Date the medication course begins (ISO `YYYY-MM-DD`). Anchors RRULE BYDAY / BYMONTHDAY patterns and the
rolling-interval countdown's first window. NULL = active from creation (the legacy implicit behaviour).
Required when `oneShot` is true.
anyOf:
- type: string
format: date
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])))$
- type: "null"
endsOn:
description: Date the medication course ends (ISO `YYYY-MM-DD`). NULL = no end date (chronic). When `oneShot` is true
the route normalises `endsOn` to equal `startsOn`.
anyOf:
- type: string
format: date
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])))$
- type: "null"
oneShot:
description: Single-administration medication (e.g. flu shot, post-op single dose). When true the medication has at most
one schedule with no `rrule` / `rollingIntervalDays`, and `active` auto-flips to false once the dose is
logged (non-skipped).
type: boolean
schedules:
type: array
items:
$ref: "#/components/schemas/MedicationScheduleInput"
reminderGraceMinutes:
description: "Detail-page bridge: primary-schedule reminder-window in minutes. The route maps the value onto the primary
schedule's `reminderGraceMinutes` field; ignored when a full `schedules` array is also supplied."
anyOf:
- type: integer
minimum: 0
maximum: 1440
- type: "null"
description: Update-medication body. Every field is optional; omitted fields are left untouched. Supplying `schedules`
REPLACES the medication's full schedule list (the route deletes existing rows before re-creating). Flipping
`active` to false records the current timestamp on `pausedAt`; flipping back to true clears it. v1.5 invariants
on the `schedules` array match the create path.
MedicationIntakeRequest:
type: object
properties:
medicationId:
type: string
minLength: 1
description: Server-narrowed from the URL path. The route layer overwrites whatever the body supplies before Zod parsing
so a caller cannot log an intake against another medication.
scheduledFor:
description: Slot the dose belongs to. Defaults to `takenAt` (or `now()` when both are absent) so the compliance pairing
logic can pin the dose to a schedule slot.
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)))$
takenAt:
description: When the dose was actually taken. NULL when `skipped` is true; defaults to `now()` for non-skipped intakes.
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)))$
skipped:
default: false
description: True to log a skipped slot (no consumption, no inventory decrement, one-shot medications stay active).
type: boolean
idempotencyKey:
description: Caller-issued de-dup key. A second POST with the same key returns the original event without creating a new
row.
type: string
maxLength: 128
required:
- medicationId
description: Per-medication intake log body. Idempotent via `idempotencyKey`; the server also dedupes by a 60-second
sliding window when the key is absent. Non-skipped intakes auto-decrement pen inventory (best-effort), refresh
the per-day compliance rollup, and — for one-shot medications — flip `active` to false.
MedicationExtractRequest:
type: object
properties:
text:
type: string
minLength: 1
maxLength: 2000
description: Free-text medication description (any locale). Up to 2 000 characters. The model never echoes the text back
into another tenant — it is only used to produce the structured fields.
locale:
description: Optional UI locale hint for the model.
type: string
enum:
- en
- de
- es
- fr
- it
- pl
today:
description: "Optional override of the reference date used to resolve relative phrases (\"tomorrow\", \"next Monday\").
Format: `YYYY-MM-DD`. Defaults to the server's UTC day."
type: string
pattern: ^\d{4}-\d{2}-\d{2}$
required:
- text
description: Free-text medication description payload. The route runs the text through the Coach provider chain and
returns a partial structured payload the wizard merges. Rate-limited 10 requests / 5 minutes / user;
budget-gated against the daily Coach token ceiling.
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.
InsightsLayoutBody:
type: object
properties:
version:
type: number
const: 1
tiles:
minItems: 1
maxItems: 50
type: array
items:
type: object
properties:
id:
type: string
enum:
- overview
- blutdruck
- puls
- sauerstoff
- koerpertemperatur
- gewicht
- bmi
- aktive-energie
- workouts
- schlaf
- ruhepuls
- hrv
- stimmung
- medikamente
visible:
type: boolean
order:
type: integer
minimum: 0
maximum: 99
required:
- id
- visible
- order
required:
- version
- tiles
description: "Per-user Insights tile layout: an ordered list of tiles with a visibility flag. `version` is the layout
schema version; tile ids are a closed enum derived from the server's tile registry."
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.
MedicationTreatmentClass:
type: string
enum:
- GENERIC
- GLP1
description: Prisma-level treatment-class discriminator. `GLP1` unlocks the GLP-1 specialist surfaces (injection-site
rotation, titration history, pen inventory, GLP-1-aware Coach).
MedicationSchedule:
type: object
properties:
id:
type: string
medicationId:
type: string
windowStart:
type: string
description: Legacy single-time-of-intake (HH:mm, user local). Preserved for backwards compatibility; the new
`timesOfDay` array supersedes it.
windowEnd:
type: string
description: Legacy reminder-window upper bound (HH:mm). Used to derive the late-classification grace span when
`reminderGraceMinutes` is null.
label:
anyOf:
- type: string
- type: "null"
dose:
anyOf:
- type: string
- type: "null"
description: Per-schedule dose override. NULL means the schedule inherits `Medication.dose`.
daysOfWeek:
anyOf:
- type: string
- type: "null"
description: Legacy persisted recurrence encoding (`null` | `1,3,5` | `i2;1,3,5`). v1.5 readers consult `rrule` first;
the field is kept for pre-v1.5 rows. v1.6.0 drops the column.
timesOfDay:
type: array
items:
type: string
description: v1.5 first-class points-in-time the dose is taken (HH:mm, user local). Backfilled to `[windowStart]` for
every pre-v1.5 row.
reminderGraceMinutes:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
description: Reminder grace window in minutes. NULL falls back to the legacy `windowEnd - windowStart` span.
rrule:
anyOf:
- type: string
- type: "null"
description: RFC 5545 RRULE string (subset). Used for calendar-anchored cadences. **Mutually exclusive with
`rollingIntervalDays`** — exactly one of the two is non-null on any v1.5+ schedule (or both are null on
legacy rows that haven't been touched since the migration).
rollingIntervalDays:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
description: Flexible-rolling interval in days, counted forward from the latest `MedicationIntakeEvent.takenAt`.
**Mutually exclusive with `rrule`.**
scheduleType:
type: string
enum:
- SCHEDULED
- PRN
- CYCLIC
description: v1.7.0 schedule-type discriminator. SCHEDULED = rrule / rolling / legacy cadence. PRN = as-needed (never
projected, reminded, or counted in compliance expected; still loggable via the intake route). CYCLIC = N
weeks on / M weeks off, gating whichever inner cadence the rrule / legacy fields describe.
cyclicOnWeeks:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
description: v1.7.0 cyclic "on" weeks. Only meaningful when `scheduleType` is CYCLIC; null otherwise.
cyclicOffWeeks:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
description: v1.7.0 cyclic "off" weeks. Only meaningful when `scheduleType` is CYCLIC; null otherwise.
required:
- id
- medicationId
- windowStart
- windowEnd
- label
- dose
- daysOfWeek
- timesOfDay
- reminderGraceMinutes
- rrule
- rollingIntervalDays
- scheduleType
- cyclicOnWeeks
- cyclicOffWeeks
description: Schedule entry attached to a medication. v1.5 promotes `timesOfDay` to first-class and introduces `rrule`
(calendar-anchored cadences) and `rollingIntervalDays` (flexible-rolling cadences). The two recurrence
primitives are mutually exclusive — enforced by the Zod refine on writes, the route layer, and a DB CHECK
constraint (`medication_schedules_rrule_xor_rolling`). v1.7.0 adds `scheduleType` (SCHEDULED / PRN / CYCLIC) and
the cyclic on/off-week fields.
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."
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`.
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
SyncStateEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/SyncStateResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
SyncStateResponse:
type: object
properties:
userId:
type: string
timezone:
type: string
lastSyncedAt:
anyOf:
- 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)))$
- type: "null"
serverNow:
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)))$
measurements:
type: object
properties:
lastUpdatedAt:
anyOf:
- 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)))$
- type: "null"
liveCount:
type: integer
minimum: 0
maximum: 9007199254740991
tombstonedCount:
type: integer
minimum: 0
maximum: 9007199254740991
required:
- lastUpdatedAt
- liveCount
- tombstonedCount
additionalProperties: false
mood:
type: object
properties:
lastUpdatedAt:
anyOf:
- 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)))$
- type: "null"
liveCount:
type: integer
minimum: 0
maximum: 9007199254740991
tombstonedCount:
type: integer
minimum: 0
maximum: 9007199254740991
required:
- lastUpdatedAt
- liveCount
- tombstonedCount
additionalProperties: false
intakes:
type: object
properties:
lastUpdatedAt:
anyOf:
- 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)))$
- type: "null"
liveCount:
type: integer
minimum: 0
maximum: 9007199254740991
tombstonedCount:
type: integer
minimum: 0
maximum: 9007199254740991
required:
- lastUpdatedAt
- liveCount
- tombstonedCount
additionalProperties: false
sync:
type: object
properties:
incrementalWindowDays:
type: integer
exclusiveMinimum: 0
maximum: 9007199254740991
description: Days an incremental delta stays valid; tracks the native refresh-token lifetime. Beyond it a device
re-pairs with a full backfill. iOS derives its window from this rather than hardcoding 60.
tombstoneRetentionDays:
type: integer
exclusiveMinimum: 0
maximum: 9007199254740991
description: Horizon past which tombstones may be pruned. A cursor older than this gets `cursorExpired` on
`/api/sync/changes`.
required:
- incrementalWindowDays
- tombstoneRetentionDays
additionalProperties: false
description: Sync-window metadata the client reads instead of hardcoding.
required:
- userId
- timezone
- lastSyncedAt
- serverNow
- measurements
- mood
- intakes
- sync
additionalProperties: false
description: iOS SyncMode handshake. Each GET also advances the server-side `lastSyncedAt` checkpoint and returns the
previous value. The cheap 'should I sync?' summary; the durable delta cursor lives on `/api/sync/changes`.
SyncChangesEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/SyncChangesResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
SyncChangesResponse:
type: object
properties:
serverNow:
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)))$
cursor:
anyOf:
- type: string
- type: "null"
description: Opaque cursor to echo into the next request.
hasMore:
type: boolean
description: False once the client is caught up as of `serverNow`.
cursorExpired:
type: boolean
description: True when the supplied cursor predates tombstone retention — drop the cursor and do a clean initial sync.
changes:
type: object
properties:
measurements:
type: object
properties:
upserts:
type: array
items:
$ref: "#/components/schemas/SyncMeasurementUpsert"
tombstones:
type: array
items:
$ref: "#/components/schemas/SyncMeasurementTombstone"
required:
- upserts
- tombstones
additionalProperties: false
mood:
type: object
properties:
upserts:
type: array
items:
$ref: "#/components/schemas/SyncMoodUpsert"
tombstones:
type: array
items:
$ref: "#/components/schemas/SyncMoodTombstone"
required:
- upserts
- tombstones
additionalProperties: false
intakes:
type: object
properties:
upserts:
type: array
items:
$ref: "#/components/schemas/SyncIntakeUpsert"
tombstones:
type: array
items:
$ref: "#/components/schemas/SyncIntakeTombstone"
required:
- upserts
- tombstones
additionalProperties: false
required:
- measurements
- mood
- intakes
additionalProperties: false
required:
- serverNow
- cursor
- hasMore
- cursorExpired
- changes
additionalProperties: false
description: "Multi-domain delta page (v1.7.0): measurements + mood + intakes. One opaque multi-domain keyset cursor;
tombstones apply before upserts within each domain. Tombstone identity: measurements key on externalId, mood +
intakes on server id. The iOS consumer is measurements-only this cycle; mood + intakes are forward-prep."
SyncMeasurementUpsert:
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
- RESPIRATORY_RATE
- BODY_MASS_INDEX
- LEAN_BODY_MASS
- WALKING_HEART_RATE_AVERAGE
- WALKING_ASYMMETRY
- WALKING_DOUBLE_SUPPORT
- WALKING_STEP_LENGTH
- WALKING_SPEED
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"
externalId:
anyOf:
- type: string
- type: "null"
description: Cross-device dedup key (UUID string or `stats:<id>:<date>`).
syncVersion:
type: integer
exclusiveMinimum: 0
maximum: 9007199254740991
description: LWW reconciliation counter; echo to keep the mirror monotonic.
updatedAt:
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
- type
- value
- unit
- measuredAt
- source
- externalId
- syncVersion
- updatedAt
additionalProperties: false
SyncMeasurementTombstone:
type: object
properties:
id:
type: string
externalId:
anyOf:
- type: string
- type: "null"
description: The identity key the client dedups on for measurements.
syncVersion:
type: integer
exclusiveMinimum: 0
maximum: 9007199254740991
deletedAt:
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)))$
updatedAt:
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
- externalId
- syncVersion
- deletedAt
- updatedAt
additionalProperties: false
description: A soft-deleted measurement. Apply tombstones BEFORE upserts within a page to avoid resurrecting a row.
SyncMoodUpsert:
type: object
properties:
id:
type: string
date:
type: string
description: YYYY-MM-DD anchored to the row's `tz`.
mood:
type: string
score:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
tags:
anyOf:
- type: string
- type: "null"
description: JSON array of tag keys, or null.
note:
anyOf:
- type: string
- type: "null"
moodLoggedAt:
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
syncVersion:
type: integer
minimum: 0
maximum: 9007199254740991
description: LWW reconciliation counter; mood is last-writer-wins by it.
updatedAt:
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
- date
- mood
- score
- tags
- note
- moodLoggedAt
- source
- syncVersion
- updatedAt
additionalProperties: false
SyncMoodTombstone:
type: object
properties:
id:
type: string
description: Server id — the identity key the client dedups on for mood.
syncVersion:
type: integer
minimum: 0
maximum: 9007199254740991
deletedAt:
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)))$
updatedAt:
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
- syncVersion
- deletedAt
- updatedAt
additionalProperties: false
description: A soft-deleted mood entry, keyed on server `id`. Apply before upserts within the domain page.
SyncIntakeUpsert:
type: object
properties:
id:
type: string
medicationId:
type: string
scheduledFor:
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)))$
takenAt:
anyOf:
- 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)))$
- type: "null"
skipped:
type: boolean
source:
type: string
syncVersion:
type: integer
minimum: 0
maximum: 9007199254740991
description: Reconciliation counter. An intake is immutable; a correction is a tombstone + re-insert.
updatedAt:
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
- medicationId
- scheduledFor
- takenAt
- skipped
- source
- syncVersion
- updatedAt
additionalProperties: false
SyncIntakeTombstone:
type: object
properties:
id:
type: string
description: Server id — the identity key the client dedups on for intakes.
syncVersion:
type: integer
minimum: 0
maximum: 9007199254740991
deletedAt:
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)))$
updatedAt:
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
- syncVersion
- deletedAt
- updatedAt
additionalProperties: false
description: A soft-deleted medication intake, keyed on server `id`. Apply before upserts within the domain page.
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
- RESPIRATORY_RATE
- BODY_MASS_INDEX
- LEAN_BODY_MASS
- WALKING_HEART_RATE_AVERAGE
- WALKING_ASYMMETRY
- WALKING_DOUBLE_SUPPORT
- WALKING_STEP_LENGTH
- WALKING_SPEED
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
ListMedicationsResponse:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/MedicationListEntry"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
MedicationListEntry:
type: object
properties:
id:
type: string
name:
type: string
dose:
type: string
treatmentClass:
$ref: "#/components/schemas/MedicationTreatmentClass"
dosesPerUnit:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
description: Doses per pen / vial for inventory tracking. NULL = inventory tracking off.
active:
type: boolean
notificationsEnabled:
type: boolean
liveActivityEnabled:
type: boolean
description: v1.7.0 iOS Live Activity opt-in for this medication's reminders. Default false. The iOS client owns the
ActivityKit lifecycle; the server only stores + echoes the flag.
criticalAlarmEnabled:
type: boolean
description: v1.7.0 iOS 26 AlarmKit critical-reminder opt-in. Default false. Critical alarms bypass the device mute
switch / Focus; the server stores the preference only.
pausedAt:
anyOf:
- 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)))$
- type: "null"
snoozedUntil:
anyOf:
- 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)))$
- type: "null"
nextDueAt:
anyOf:
- 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)))$
- type: "null"
description: v1.7.0 server-computed next due instant across all the medication's schedules (earliest
`nextOccurrenceAfter`). Read-only — computed, not stored. NULL when no schedule has an upcoming slot
(paused, one-shot in the past, `endsOn` crossed, every schedule PRN). The list GET is cached 60 s, so a 60 s
staleness is accepted.
startsOn:
anyOf:
- 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)))$
- type: "null"
description: v1.5 course start (ISO date). Anchors RRULE BYDAY / BYMONTHDAY patterns and the rolling-interval
countdown's first window. NULL means active from creation.
endsOn:
anyOf:
- 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)))$
- type: "null"
description: v1.5 course end (ISO date). NULL means chronic. Equals `startsOn` when `oneShot` is true.
oneShot:
type: boolean
description: v1.5 single-administration flag. When true the medication has at most one schedule (no `rrule` /
`rollingIntervalDays`), and `active` auto-flips to false once the dose is logged.
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)))$
updatedAt:
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)))$
schedules:
type: array
items:
$ref: "#/components/schemas/MedicationScheduleOutput"
category:
$ref: "#/components/schemas/MedicationCategory"
lastTakenAt:
anyOf:
- 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)))$
- type: "null"
description: Latest non-skipped `MedicationIntakeEvent.takenAt` for the medication. Drives the rolling-cadence countdown
surface.
todayEventCount:
type: integer
minimum: 0
maximum: 9007199254740991
description: Number of intake events scheduled for today (user-local day window).
required:
- id
- name
- dose
- treatmentClass
- dosesPerUnit
- active
- notificationsEnabled
- liveActivityEnabled
- criticalAlarmEnabled
- pausedAt
- snoozedUntil
- nextDueAt
- startsOn
- endsOn
- oneShot
- createdAt
- updatedAt
- schedules
- category
- lastTakenAt
- todayEventCount
additionalProperties: false
description: List-row variant of the medication resource enriched with the joined `category`, `lastTakenAt`, and
`todayEventCount` fields the dashboard + iOS client consume. The base medication fields (`id`, `name`, `dose`,
`treatmentClass`, `dosesPerUnit`, `active`, `notificationsEnabled`, `pausedAt`, `snoozedUntil`, `startsOn`,
`endsOn`, `oneShot`, `createdAt`, `updatedAt`, `schedules`) are inlined; see the `Medication` component for
their semantics.
MedicationCategory:
type: string
enum:
- BLOOD_PRESSURE
- VITAMIN
- SUPPLEMENT
- PAIN_RELIEF
- ALLERGY
- DIGESTIVE
- THYROID
- HORMONE
- SKIN
- SLEEP_AID
- DIABETES
- ANTIBIOTIC
- OTHER
description: Clinical taxonomy stored in the `medication_categories` side-table. Orthogonal to `MedicationTreatmentClass`.
CreateMedicationResponse:
type: object
properties:
data:
$ref: "#/components/schemas/MedicationDetail"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
MedicationDetail:
type: object
properties:
id:
type: string
name:
type: string
dose:
type: string
treatmentClass:
$ref: "#/components/schemas/MedicationTreatmentClass"
dosesPerUnit:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
description: Doses per pen / vial for inventory tracking. NULL = inventory tracking off.
active:
type: boolean
notificationsEnabled:
type: boolean
liveActivityEnabled:
type: boolean
description: v1.7.0 iOS Live Activity opt-in for this medication's reminders. Default false. The iOS client owns the
ActivityKit lifecycle; the server only stores + echoes the flag.
criticalAlarmEnabled:
type: boolean
description: v1.7.0 iOS 26 AlarmKit critical-reminder opt-in. Default false. Critical alarms bypass the device mute
switch / Focus; the server stores the preference only.
pausedAt:
anyOf:
- 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)))$
- type: "null"
snoozedUntil:
anyOf:
- 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)))$
- type: "null"
nextDueAt:
anyOf:
- 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)))$
- type: "null"
description: v1.7.0 server-computed next due instant across all the medication's schedules (earliest
`nextOccurrenceAfter`). Read-only — computed, not stored. NULL when no schedule has an upcoming slot
(paused, one-shot in the past, `endsOn` crossed, every schedule PRN). The list GET is cached 60 s, so a 60 s
staleness is accepted.
startsOn:
anyOf:
- 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)))$
- type: "null"
description: v1.5 course start (ISO date). Anchors RRULE BYDAY / BYMONTHDAY patterns and the rolling-interval
countdown's first window. NULL means active from creation.
endsOn:
anyOf:
- 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)))$
- type: "null"
description: v1.5 course end (ISO date). NULL means chronic. Equals `startsOn` when `oneShot` is true.
oneShot:
type: boolean
description: v1.5 single-administration flag. When true the medication has at most one schedule (no `rrule` /
`rollingIntervalDays`), and `active` auto-flips to false once the dose is logged.
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)))$
updatedAt:
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)))$
schedules:
type: array
items:
$ref: "#/components/schemas/MedicationScheduleOutput"
category:
$ref: "#/components/schemas/MedicationCategory"
required:
- id
- name
- dose
- treatmentClass
- dosesPerUnit
- active
- notificationsEnabled
- liveActivityEnabled
- criticalAlarmEnabled
- pausedAt
- snoozedUntil
- nextDueAt
- startsOn
- endsOn
- oneShot
- createdAt
- updatedAt
- schedules
- category
additionalProperties: false
description: Detail variant of the medication resource enriched with the joined `category`. The base medication fields
are inlined; see the `Medication` component for their semantics.
GetMedicationResponse:
type: object
properties:
data:
$ref: "#/components/schemas/MedicationDetail"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
UpdateMedicationResponse:
type: object
properties:
data:
$ref: "#/components/schemas/MedicationDetail"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
DeleteMedicationResponse:
type: object
properties:
data:
type: object
properties:
deleted:
type: boolean
required:
- deleted
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
ReplayMedicationIntakeResponse:
type: object
properties:
data:
$ref: "#/components/schemas/MedicationIntakeEvent"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
MedicationIntakeEvent:
type: object
properties:
id:
type: string
userId:
type: string
medicationId:
type: string
scheduledFor:
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)))$
takenAt:
anyOf:
- 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)))$
- type: "null"
skipped:
type: boolean
source:
type: string
enum:
- WEB
- API
- REMINDER
- IMPORT
idempotencyKey:
anyOf:
- type: string
- type: "null"
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
- userId
- medicationId
- scheduledFor
- takenAt
- skipped
- source
- idempotencyKey
- createdAt
additionalProperties: false
description: Single dose log row. `takenAt` is non-null for confirmed intakes; `skipped:true` represents a
deliberately-missed dose (no inventory consumption).
CreateMedicationIntakeResponse:
type: object
properties:
data:
$ref: "#/components/schemas/MedicationIntakeEvent"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
MedicationExtractResponse:
type: object
properties:
data:
type: object
properties:
name:
type: string
minLength: 1
maxLength: 100
dose:
type: string
minLength: 1
maxLength: 50
doseUnit:
type: string
enum:
- mg
- ml
- iu
- tablets
- drops
- puffs
- sprays
cadenceKind:
type: string
enum:
- daily
- weekdays
- everyNWeeks
- monthly
- everyNMonths
- yearly
- rolling
- oneShot
weekdays:
maxItems: 7
type: array
items:
type: string
enum:
- MO
- TU
- WE
- TH
- FR
- SA
- SU
intervalWeeks:
type: integer
minimum: 1
maximum: 52
intervalMonths:
type: integer
minimum: 1
maximum: 12
dayOfMonth:
type: integer
minimum: 1
maximum: 31
rollingIntervalDays:
type: integer
minimum: 1
maximum: 365
timesOfDay:
maxItems: 12
type: array
items:
type: string
pattern: ^([01]\d|2[0-3]):[0-5]\d$
startsOn:
type: string
pattern: ^\d{4}-\d{2}-\d{2}$
endsOn:
type: string
pattern: ^\d{4}-\d{2}-\d{2}$
oneShot:
type: boolean
confidence:
type: object
propertyNames:
type: string
additionalProperties:
type: number
minimum: 0
maximum: 1
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
GetMedicationCadenceResponse:
type: object
properties:
data:
$ref: "#/components/schemas/MedicationCadenceResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
MedicationCadenceResponse:
type: object
properties:
windowDays:
type: integer
exclusiveMinimum: 0
maximum: 9007199254740991
anchorIso:
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)))$
next:
anyOf:
- type: object
properties:
windowStart:
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)))$
windowEnd:
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)))$
scheduleIndex:
type: integer
minimum: 0
maximum: 9007199254740991
required:
- windowStart
- windowEnd
- scheduleIndex
additionalProperties: false
- type: "null"
chips:
$ref: "#/components/schemas/MedicationCadenceChips"
timeline:
type: array
items:
$ref: "#/components/schemas/MedicationCadenceTimelinePoint"
required:
- windowDays
- anchorIso
- next
- chips
- timeline
additionalProperties: false
description: Cadence + compliance read for a single medication. `next` is the upcoming-dose envelope (null when the
course has ended or the rolling clock has no pinning intake yet); `timeline` walks the requested `windowDays`
worth of slots in ascending time order.
MedicationCadenceChips:
type: object
properties:
adherenceRate:
type: number
streakDays:
type: integer
minimum: 0
maximum: 9007199254740991
expectedSlots:
type: integer
minimum: 0
maximum: 9007199254740991
actualDoses:
type: integer
minimum: 0
maximum: 9007199254740991
required:
- adherenceRate
- streakDays
- expectedSlots
- actualDoses
additionalProperties: false
description: Four compliance summary values for the medication detail page chip row.
MedicationCadenceTimelinePoint:
type: object
properties:
day:
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)))$
windowStart:
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)))$
windowEnd:
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)))$
scheduleIndex:
type: integer
minimum: 0
maximum: 9007199254740991
status:
type: string
required:
- day
- windowStart
- windowEnd
- scheduleIndex
- status
additionalProperties: false
description: One expected-vs-actual dose slot for the cadence timeline chart. `status` is one of `taken | skipped |
missed | pending | future` and drives the chip colour.
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
dataClusters:
maxItems: 10
type: array
items:
type: string
enum:
- cardio
- body
- activity
- workouts
- sleep
- mood
- glucose
- medication
- mobility
- environment
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
dataClusters:
maxItems: 10
type: array
items:
type: string
enum:
- cardio
- body
- activity
- workouts
- sleep
- mood
- glucose
- medication
- mobility
- environment
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
UploadAvatarResponse:
type: object
properties:
data:
type: object
properties:
avatarUrl:
type: string
contentType:
type: string
enum:
- image/jpeg
- image/png
- image/webp
updatedAt:
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:
- avatarUrl
- contentType
- updatedAt
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
InsightsLayout:
type: object
properties:
data:
$ref: "#/components/schemas/InsightsLayoutBodyOutput"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
InsightsLayoutSaved:
type: object
properties:
data:
$ref: "#/components/schemas/InsightsLayoutBodyOutput"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
InsightsLayoutReset:
type: object
properties:
data:
$ref: "#/components/schemas/InsightsLayoutBodyOutput"
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.
DashboardSnapshotResponseEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/DashboardSnapshotResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
DashboardSnapshotResponse:
type: object
properties:
user:
type: object
properties:
username:
type: string
timezone:
type: string
heightCm:
anyOf:
- type: number
- type: "null"
dateOfBirth:
anyOf:
- type: string
- type: "null"
gender:
anyOf:
- type: string
enum:
- MALE
- FEMALE
- type: "null"
glucoseUnit:
anyOf:
- type: string
- type: "null"
onboardingTourCompleted:
type: boolean
greetingHour:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
required:
- username
- timezone
- heightCm
- dateOfBirth
- gender
- glucoseUnit
- onboardingTourCompleted
- greetingHour
additionalProperties: false
layout:
type: object
propertyNames:
type: string
additionalProperties: {}
layoutCatalogue:
type: array
items:
type: object
properties:
id:
type: string
visible:
type: boolean
order:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
required:
- id
- visible
- order
additionalProperties: false
description: Full 27-id widget catalogue (server-known + iOS-only) with per-widget visibility + order. iOS-only ids are
appended default-invisible. The web dashboard reads `layout`; this block is the cold-launch seed for the
native client.
metricStates:
type: object
propertyNames:
type: string
additionalProperties:
type: object
properties:
value:
type: number
measuredAt:
type: string
unit:
type: string
required:
- value
- measuredAt
- unit
additionalProperties: false
description: "Latest reading per chartable metric, keyed by the iOS `MetricKind` raw value (the non-obvious raws:
`oxygenSaturation`, `totalBodyWater`, `heartRateVariability`, `bodyMassIndex`, `walkingAsymmetryPercentage`,
`walkingDoubleSupportPercentage`, `environmentalAudioExposure`, `headphoneAudioExposure`,
`activeEnergyBurned`). Each entry carries `value`, `measuredAt` (ISO8601), and the canonical `unit`. Types
the user has never logged are omitted."
tiles:
type: object
properties:
summaries:
type: object
propertyNames:
type: string
additionalProperties: {}
lastSeenByType:
type: object
propertyNames:
type: string
additionalProperties: {}
mood:
type: object
properties:
summary:
anyOf:
- type: object
propertyNames:
type: string
additionalProperties: {}
- type: "null"
entries:
type: array
items:
type: object
properties:
date:
type: string
score:
type: number
samples:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
required:
- date
- score
- samples
additionalProperties: false
required:
- summary
- entries
additionalProperties: false
required:
- summaries
- lastSeenByType
- mood
additionalProperties: false
extras:
anyOf:
- type: object
properties:
bpInTargetPct:
anyOf:
- type: number
- type: "null"
bpInTargetPct7d:
anyOf:
- type: number
- type: "null"
bpInTargetPct30d:
anyOf:
- type: number
- type: "null"
bpInTargetPctAllTime:
anyOf:
- type: number
- type: "null"
bpInTargetPctPriorMonth:
anyOf:
- type: number
- type: "null"
bpInTargetPctPriorYear:
anyOf:
- type: number
- type: "null"
glucoseByContext:
type: object
propertyNames:
type: string
additionalProperties: {}
required:
- bpInTargetPct
- bpInTargetPct7d
- bpInTargetPct30d
- bpInTargetPctAllTime
- bpInTargetPctPriorMonth
- bpInTargetPctPriorYear
- glucoseByContext
additionalProperties: false
- type: "null"
briefing:
anyOf:
- type: object
propertyNames:
type: string
additionalProperties: {}
- type: "null"
briefingState:
type: string
enum:
- ready
- preparing
- disabled
briefingUpdatedAt:
anyOf:
- type: string
- type: "null"
generatedAt:
type: string
required:
- user
- layout
- layoutCatalogue
- metricStates
- tiles
- extras
- briefing
- briefingState
- briefingUpdatedAt
- generatedAt
additionalProperties: false
description: Unified above-the-fold dashboard payload. `tiles` always arrives (slim summaries + mood + resolved widget
layout); `extras` (BD-in-target + per-context glucose) is null on a rollup-coverage miss so the strip never
waits on the slowest read. `briefing` is lifted read-only from the pre-generated insight cache — never generated
synchronously — and reports `ready` / `preparing` / `disabled` via `briefingState`. `layoutCatalogue` (full
27-id widget catalogue) and `metricStates` (latest reading per metric, keyed by iOS `MetricKind` raw value) are
additive cold-launch seeds for the native client; both derive in-process from data already fetched, adding no DB
round-trip.
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.
MedicationScheduleOutput:
type: object
properties:
id:
type: string
medicationId:
type: string
windowStart:
type: string
description: Legacy single-time-of-intake (HH:mm, user local). Preserved for backwards compatibility; the new
`timesOfDay` array supersedes it.
windowEnd:
type: string
description: Legacy reminder-window upper bound (HH:mm). Used to derive the late-classification grace span when
`reminderGraceMinutes` is null.
label:
anyOf:
- type: string
- type: "null"
dose:
anyOf:
- type: string
- type: "null"
description: Per-schedule dose override. NULL means the schedule inherits `Medication.dose`.
daysOfWeek:
anyOf:
- type: string
- type: "null"
description: Legacy persisted recurrence encoding (`null` | `1,3,5` | `i2;1,3,5`). v1.5 readers consult `rrule` first;
the field is kept for pre-v1.5 rows. v1.6.0 drops the column.
timesOfDay:
type: array
items:
type: string
description: v1.5 first-class points-in-time the dose is taken (HH:mm, user local). Backfilled to `[windowStart]` for
every pre-v1.5 row.
reminderGraceMinutes:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
description: Reminder grace window in minutes. NULL falls back to the legacy `windowEnd - windowStart` span.
rrule:
anyOf:
- type: string
- type: "null"
description: RFC 5545 RRULE string (subset). Used for calendar-anchored cadences. **Mutually exclusive with
`rollingIntervalDays`** — exactly one of the two is non-null on any v1.5+ schedule (or both are null on
legacy rows that haven't been touched since the migration).
rollingIntervalDays:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
description: Flexible-rolling interval in days, counted forward from the latest `MedicationIntakeEvent.takenAt`.
**Mutually exclusive with `rrule`.**
scheduleType:
type: string
enum:
- SCHEDULED
- PRN
- CYCLIC
description: v1.7.0 schedule-type discriminator. SCHEDULED = rrule / rolling / legacy cadence. PRN = as-needed (never
projected, reminded, or counted in compliance expected; still loggable via the intake route). CYCLIC = N
weeks on / M weeks off, gating whichever inner cadence the rrule / legacy fields describe.
cyclicOnWeeks:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
description: v1.7.0 cyclic "on" weeks. Only meaningful when `scheduleType` is CYCLIC; null otherwise.
cyclicOffWeeks:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
description: v1.7.0 cyclic "off" weeks. Only meaningful when `scheduleType` is CYCLIC; null otherwise.
required:
- id
- medicationId
- windowStart
- windowEnd
- label
- dose
- daysOfWeek
- timesOfDay
- reminderGraceMinutes
- rrule
- rollingIntervalDays
- scheduleType
- cyclicOnWeeks
- cyclicOffWeeks
additionalProperties: false
description: Schedule entry attached to a medication. v1.5 promotes `timesOfDay` to first-class and introduces `rrule`
(calendar-anchored cadences) and `rollingIntervalDays` (flexible-rolling cadences). The two recurrence
primitives are mutually exclusive — enforced by the Zod refine on writes, the route layer, and a DB CHECK
constraint (`medication_schedules_rrule_xor_rolling`). v1.7.0 adds `scheduleType` (SCHEDULED / PRN / CYCLIC) and
the cyclic on/off-week fields.
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.
InsightsLayoutBodyOutput:
type: object
properties:
version:
type: number
const: 1
tiles:
minItems: 1
maxItems: 50
type: array
items:
type: object
properties:
id:
type: string
enum:
- overview
- blutdruck
- puls
- sauerstoff
- koerpertemperatur
- gewicht
- bmi
- aktive-energie
- workouts
- schlaf
- ruhepuls
- hrv
- stimmung
- medikamente
visible:
type: boolean
order:
type: integer
minimum: 0
maximum: 99
required:
- id
- visible
- order
additionalProperties: false
required:
- version
- tiles
additionalProperties: false
description: "Per-user Insights tile layout: an ordered list of tiles with a visibility flag. `version` is the layout
schema version; tile ids are a closed enum derived from the server's tile registry."
Medication:
type: object
properties:
id:
type: string
name:
type: string
dose:
type: string
treatmentClass:
$ref: "#/components/schemas/MedicationTreatmentClass"
dosesPerUnit:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
description: Doses per pen / vial for inventory tracking. NULL = inventory tracking off.
active:
type: boolean
notificationsEnabled:
type: boolean
liveActivityEnabled:
type: boolean
description: v1.7.0 iOS Live Activity opt-in for this medication's reminders. Default false. The iOS client owns the
ActivityKit lifecycle; the server only stores + echoes the flag.
criticalAlarmEnabled:
type: boolean
description: v1.7.0 iOS 26 AlarmKit critical-reminder opt-in. Default false. Critical alarms bypass the device mute
switch / Focus; the server stores the preference only.
pausedAt:
anyOf:
- 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)))$
- type: "null"
snoozedUntil:
anyOf:
- 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)))$
- type: "null"
nextDueAt:
anyOf:
- 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)))$
- type: "null"
description: v1.7.0 server-computed next due instant across all the medication's schedules (earliest
`nextOccurrenceAfter`). Read-only — computed, not stored. NULL when no schedule has an upcoming slot
(paused, one-shot in the past, `endsOn` crossed, every schedule PRN). The list GET is cached 60 s, so a 60 s
staleness is accepted.
startsOn:
anyOf:
- 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)))$
- type: "null"
description: v1.5 course start (ISO date). Anchors RRULE BYDAY / BYMONTHDAY patterns and the rolling-interval
countdown's first window. NULL means active from creation.
endsOn:
anyOf:
- 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)))$
- type: "null"
description: v1.5 course end (ISO date). NULL means chronic. Equals `startsOn` when `oneShot` is true.
oneShot:
type: boolean
description: v1.5 single-administration flag. When true the medication has at most one schedule (no `rrule` /
`rollingIntervalDays`), and `active` auto-flips to false once the dose is logged.
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)))$
updatedAt:
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)))$
schedules:
type: array
items:
$ref: "#/components/schemas/MedicationScheduleOutput"
required:
- id
- name
- dose
- treatmentClass
- dosesPerUnit
- active
- notificationsEnabled
- liveActivityEnabled
- criticalAlarmEnabled
- pausedAt
- snoozedUntil
- nextDueAt
- startsOn
- endsOn
- oneShot
- createdAt
- updatedAt
- schedules
additionalProperties: false
description: Server-shaped medication row returned by GET / POST / PUT endpoints. Carries the v1.5 course-window fields
(`startsOn`, `endsOn`, `oneShot`) at the medication level and the per-schedule cadence fields on the nested
`schedules` array.
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.