Files
HealthLog/docs/api/openapi.yaml
T
Marc-André BombeckandClaude Opus 4.7 be7a2f135f fix(meds): tighten the v1.5 schedule schema invariants
Adds three Zod refines that close gaps the audit surfaced: one-shot
medications now require a startsOn, course windows refuse endsOn
before startsOn, and rolling-cadence schedules cap timesOfDay at one
entry. The wizard's reminders toggle is now part of the create
payload so the user's choice actually ships.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 10:23:28 +02:00

4377 lines
146 KiB
YAML

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