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::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.