openapi: 3.1.0 info: title: HealthLog API version: 1.16.15 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: PolyForm-Noncommercial-1.0.0 url: https://github.com/MBombeck/HealthLog/blob/main/LICENSE contact: name: HealthLog url: https://healthlog.dev servers: - url: https://healthlog.example.com description: Your self-hosted instance - url: http://localhost:3000 description: Local dev tags: - name: Auth - name: Measurements - name: Medications - name: Mood - name: Analytics - name: Insights - name: Consent - name: Dashboard - name: Notifications - name: Devices - name: Export - name: Sync - name: Cycle - name: Admin - name: Meta paths: /api/cycle/day-logs: get: tags: - Cycle summary: Read a single day's cycle day-log (v1.15.0) description: Returns the full `CycleDayLogDTO` for the tz-anchored `date`, or `null` when nothing is logged that day. Lets a client pre-fill an edit sheet. Gated; owner-scoped; soft-deleted rows excluded. parameters: - in: query name: date schema: type: string pattern: ^\d{4}-\d{2}-\d{2}$ required: true responses: "200": description: The day-log for that date, or null. content: application/json: schema: $ref: "#/components/schemas/CycleDayLogReadEnvelope" "401": &a1 description: Authentication required or invalid credentials. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "403": &a2 description: Cycle tracking is not enabled for this account (errorCode `cycle.disabled`). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": &a3 description: Request validation failed. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "429": &a4 description: Rate limit exceeded. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" post: tags: - Cycle summary: Capture a single cycle day-log (v1.15.0) description: "Upserts on `(userId, source, externalId)` when externalId present, else `(userId, date)`. `note` encrypts at rest. 201 on insert, 200 on update. Gated: `cycle.disabled` 403 when the feature is off." requestBody: required: true content: application/json: schema: type: object properties: date: type: string pattern: ^\d{4}-\d{2}-\d{2}$ flow: type: string enum: - NONE - SPOTTING - LIGHT - MEDIUM - HEAVY intermenstrualBleeding: type: boolean basalBodyTempC: type: number minimum: 30 maximum: 45 temperatureExcluded: type: boolean ovulationTest: type: string enum: - NEGATIVE - POSITIVE_LH_SURGE - ESTROGEN_SURGE - INDETERMINATE cervicalMucus: type: string enum: - DRY - STICKY - CREAMY - WATERY - EGG_WHITE cervixPosition: type: string enum: - LOW - HIGH cervixFirmness: type: string enum: - FIRM - SOFT cervixOpening: type: string enum: - CLOSED - OPEN sexualActivity: type: boolean protectedSex: anyOf: - type: boolean - type: "null" pregnancyTest: type: string enum: - NEGATIVE - POSITIVE - INDETERMINATE progesteroneTest: type: string enum: - NEGATIVE - POSITIVE - INDETERMINATE contraceptive: type: string enum: - NONE - UNSPECIFIED - IMPLANT - INJECTION - IUD - INTRAVAGINAL_RING - ORAL - PATCH - EMERGENCY symptoms: maxItems: 40 type: array items: type: object properties: key: type: string minLength: 1 maxLength: 80 severity: type: integer minimum: 1 maximum: 4 required: - key note: type: string maxLength: 500 loggedAt: 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: default: MANUAL type: string enum: - MANUAL - APPLE_HEALTH externalId: type: string minLength: 1 maxLength: 120 required: - date - loggedAt responses: "200": description: Existing day-log updated. content: application/json: schema: $ref: "#/components/schemas/CycleDayLogEnvelope" "201": description: New day-log created. content: application/json: schema: $ref: "#/components/schemas/CycleDayLogCreatedEnvelope" "401": *a1 "403": *a2 "422": *a3 "429": *a4 /api/cycle/day-logs/{id}: patch: tags: - Cycle summary: Edit a single cycle day-log (v1.15.0) description: Partial edit; an omitted field is left untouched. Owner-scoped (a cross-user id 404s). Gated. parameters: - in: path name: id schema: type: string required: true requestBody: required: true content: application/json: schema: type: object properties: flow: anyOf: - type: string enum: - NONE - SPOTTING - LIGHT - MEDIUM - HEAVY - type: "null" intermenstrualBleeding: type: boolean basalBodyTempC: anyOf: - type: number minimum: 30 maximum: 45 - type: "null" temperatureExcluded: type: boolean ovulationTest: anyOf: - type: string enum: - NEGATIVE - POSITIVE_LH_SURGE - ESTROGEN_SURGE - INDETERMINATE - type: "null" cervicalMucus: anyOf: - type: string enum: - DRY - STICKY - CREAMY - WATERY - EGG_WHITE - type: "null" cervixPosition: anyOf: - type: string enum: - LOW - HIGH - type: "null" cervixFirmness: anyOf: - type: string enum: - FIRM - SOFT - type: "null" cervixOpening: anyOf: - type: string enum: - CLOSED - OPEN - type: "null" sexualActivity: type: boolean protectedSex: anyOf: - type: boolean - type: "null" pregnancyTest: anyOf: - type: string enum: - NEGATIVE - POSITIVE - INDETERMINATE - type: "null" progesteroneTest: anyOf: - type: string enum: - NEGATIVE - POSITIVE - INDETERMINATE - type: "null" contraceptive: anyOf: - type: string enum: - NONE - UNSPECIFIED - IMPLANT - INJECTION - IUD - INTRAVAGINAL_RING - ORAL - PATCH - EMERGENCY - type: "null" symptoms: maxItems: 40 type: array items: type: object properties: key: type: string minLength: 1 maxLength: 80 severity: type: integer minimum: 1 maximum: 4 required: - key note: anyOf: - type: string maxLength: 500 - type: "null" loggedAt: 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)))$ responses: "200": description: Day-log updated. content: application/json: schema: $ref: "#/components/schemas/CycleDayLogPatchEnvelope" "401": *a1 "403": *a2 "404": description: Day-log not found / not owned. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 delete: tags: - Cycle summary: Soft-delete a cycle day-log (v1.15.0) description: Sets `deletedAt` + bumps `syncVersion`; surfaces as a tombstone on the next `/api/sync/changes` page. 204. Idempotent. parameters: - in: path name: id schema: type: string required: true responses: "204": description: Soft-deleted (no body). "401": *a1 "403": *a2 "404": description: Day-log not found / not owned. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/cycle/day-logs/bulk: post: tags: - Cycle summary: Bulk drain cycle day-logs (Outbox / HealthKit) (v1.15.0) description: "Up to 500 entries; `withIdempotency`; rate-limited `cycle:day-logs:bulk:` 60/min. Per-entry status: inserted | duplicate | updated | skipped. Always 200." requestBody: required: true content: application/json: schema: type: object properties: entries: minItems: 1 maxItems: 500 type: array items: type: object properties: date: type: string pattern: ^\d{4}-\d{2}-\d{2}$ flow: type: string enum: - NONE - SPOTTING - LIGHT - MEDIUM - HEAVY intermenstrualBleeding: type: boolean basalBodyTempC: type: number minimum: 30 maximum: 45 temperatureExcluded: type: boolean ovulationTest: type: string enum: - NEGATIVE - POSITIVE_LH_SURGE - ESTROGEN_SURGE - INDETERMINATE cervicalMucus: type: string enum: - DRY - STICKY - CREAMY - WATERY - EGG_WHITE cervixPosition: type: string enum: - LOW - HIGH cervixFirmness: type: string enum: - FIRM - SOFT cervixOpening: type: string enum: - CLOSED - OPEN sexualActivity: type: boolean protectedSex: anyOf: - type: boolean - type: "null" pregnancyTest: type: string enum: - NEGATIVE - POSITIVE - INDETERMINATE progesteroneTest: type: string enum: - NEGATIVE - POSITIVE - INDETERMINATE contraceptive: type: string enum: - NONE - UNSPECIFIED - IMPLANT - INJECTION - IUD - INTRAVAGINAL_RING - ORAL - PATCH - EMERGENCY symptoms: maxItems: 40 type: array items: type: object properties: key: type: string minLength: 1 maxLength: 80 severity: type: integer minimum: 1 maximum: 4 required: - key note: type: string maxLength: 500 loggedAt: 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: default: MANUAL type: string enum: - MANUAL - APPLE_HEALTH externalId: type: string minLength: 1 maxLength: 120 required: - date - loggedAt required: - entries responses: "200": description: Batch processed (per-entry results). content: application/json: schema: $ref: "#/components/schemas/CycleBulkEnvelope" "401": *a1 "403": *a2 "422": *a3 "429": *a4 /api/cycle/period: post: tags: - Cycle summary: Period-boundary shortcut (v1.15.0) description: One-tap started/ended period. `start` opens a new cycle (closing the prior); `end` stamps periodEndDate. Writes the boundary day-log. Gated. requestBody: required: true content: application/json: schema: type: object properties: action: type: string enum: - start - end date: type: string pattern: ^\d{4}-\d{2}-\d{2}$ externalId: type: string minLength: 1 maxLength: 120 loggedAt: 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: - action - date - loggedAt responses: "200": description: Cycle + boundary day-log. content: application/json: schema: $ref: "#/components/schemas/CyclePeriodEnvelope" "401": *a1 "403": *a2 "422": *a3 "429": *a4 /api/cycle/calendar: get: tags: - Cycle summary: Predicted cycle calendar (v1.15.0) description: "Runs the deterministic engine to build `{ profile, prediction, days }`. Fertile-window fields are server-suppressed unless goal is TRYING_TO_CONCEIVE. Default range: today − 90d … +180d. Gated." parameters: - in: query name: from schema: type: string - in: query name: to schema: type: string responses: "200": description: Calendar grid + forecast. content: application/json: schema: $ref: "#/components/schemas/CycleCalendarEnvelope" "401": *a1 "403": *a2 "422": *a3 "429": *a4 /api/cycle/cycles: get: tags: - Cycle summary: Cycle history + stats (v1.15.0) description: Most-recent cycles (newest first) + `{ avgLengthDays, lengthVariabilityDays (MAD), avgPeriodLengthDays, regularity }`. `limit` default 24. Gated. parameters: - in: query name: limit schema: type: integer minimum: 1 maximum: 60 responses: "200": description: Cycle history. content: application/json: schema: $ref: "#/components/schemas/CycleHistoryEnvelope" "401": *a1 "403": *a2 "422": *a3 "429": *a4 /api/cycle/insights: get: tags: - Cycle summary: Cycle-phase correlation insights (v1.15.0) description: "FDR-guarded luteal-vs-follicular phase contrast per outcome metric (RHR / HRV / sleep / steps / weight / temperatures), plus the single headline finding (resting-heart-rate-by-phase, falling back to HRV). The same Welch t-test + Benjamini-Hochberg machinery the mood-factor crosstab runs; only rows with p < 0.05 AND q ≤ 0.10 surface. Strictly gender-gated — phase never appears on the general `/api/insights/correlations` route. Observational only, never causal. Gated: `cycle.disabled` 403 when the feature is off." responses: "200": description: Phase-correlation rows + headline. content: application/json: schema: $ref: "#/components/schemas/CycleInsightsEnvelope" "401": *a1 "403": *a2 "422": *a3 "429": *a4 /api/cycle/cycles/{id}: delete: tags: - Cycle summary: Soft-delete a menstrual cycle (v1.15.0) description: Sets `deletedAt` + bumps `syncVersion`; tombstones on the next sync page. 204. Idempotent. parameters: - in: path name: id schema: type: string required: true responses: "204": description: Soft-deleted (no body). "401": *a1 "403": *a2 "404": description: Cycle not found / not owned. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/cycle/all: delete: tags: - Cycle summary: Hard-purge all cycle data (v1.15.0) description: "One-click privacy purge: HARD-deletes every cycle day-log (+ symptom links by cascade), menstrual cycle, prediction, the cycle audit trail, and the cycle reminder rows in the push-attempts ledger — no dated reproductive trace survives. The CycleProfile row is left in place. Gated + owner-scoped + audited." responses: "200": description: Purge counts. content: application/json: schema: $ref: "#/components/schemas/CyclePurgeEnvelope" "401": *a1 "403": *a2 "422": *a3 "429": *a4 /api/cycle/profile: get: tags: - Cycle summary: Read the full cycle profile (v1.15.0) description: Returns the resolved CycleProfileDTO. Gated. responses: "200": description: Cycle profile. content: application/json: schema: $ref: "#/components/schemas/CycleProfileEnvelope" "401": *a1 "403": *a2 "422": *a3 "429": *a4 /api/auth/me/cycle-prefs: get: tags: - Cycle summary: Read cycle preferences (v1.15.0) description: Returns the resolved CycleProfileDTO. NOT gated — this is the surface that flips the gate. responses: "200": description: Cycle preferences. content: application/json: schema: $ref: "#/components/schemas/CyclePrefsGetEnvelope" "401": *a1 "422": *a3 "429": *a4 patch: tags: - Cycle summary: Update cycle preferences (v1.15.0) description: Deep-merges the supplied fields. `enabled` flips `cycleTrackingEnabled`. Returns the merged CycleProfileDTO. NOT gated. requestBody: required: true content: application/json: schema: type: object properties: enabled: type: boolean goal: type: string enum: - GENERAL_HEALTH - AVOID_PREGNANCY - TRYING_TO_CONCEIVE - PERIMENOPAUSE - OFF secondarySymptom: type: string enum: - MUCUS - CERVIX rawChartMode: type: boolean predictionEnabled: type: boolean discreetNotifications: type: boolean sensitiveCategoryEncryption: type: boolean typicalCycleLength: anyOf: - type: integer minimum: 15 maximum: 60 - type: "null" typicalPeriodLength: anyOf: - type: integer minimum: 1 maximum: 15 - type: "null" lutealPhaseLength: anyOf: - type: integer minimum: 10 maximum: 16 - type: "null" responses: "200": description: Merged cycle preferences. content: application/json: schema: $ref: "#/components/schemas/CyclePrefsPatchEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/meta/capabilities: get: tags: - Meta summary: Live server capability / id-vocabulary discovery description: Returns the server's REAL id vocabularies (derived-metric ids, vitals-baseline types, layout tile-ids, metric-status ids, the HealthKit ingest mapping, the FHIR coding constants) plus the running API contract version. Every list is derived server-side from the canonical registry it documents, so a client can gate its UI / decoder against what the server actually ships rather than a hand-maintained copy. Auth via cookie or Bearer (not admin). responses: "200": description: Capability snapshot. content: application/json: schema: $ref: "#/components/schemas/CapabilitiesEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/export/health-record: post: tags: - Export summary: Generate a health-record export (PDF / FHIR / package) description: "v1.7.0 flagship export. Returns the doctor-handover artefact in the requested `format`: `pdf` → application/pdf, `fhir` → application/fhir+json (HL7 FHIR R4 document Bundle), `package` → application/zip (PDF + FHIR + README). Auth via cookie or Bearer; shared `export:` rate bucket (10/h). Strict validation: unknown keys 422." requestBody: required: true content: application/json: schema: type: object properties: format: type: string enum: - pdf - fhir - package range: type: object properties: startDate: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ endDate: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ days: type: integer minimum: 1 maximum: 365 additionalProperties: false sections: type: object properties: vitals: type: object properties: weight: type: boolean bp: type: boolean pulse: type: boolean oxygenSaturation: type: boolean bodyFat: type: boolean bodyComposition: type: boolean cardioFitness: type: object properties: restingHeartRate: type: boolean hrv: type: boolean vo2max: type: boolean activity: type: object properties: steps: type: boolean distance: type: boolean energy: type: boolean sleep: type: boolean glucose: type: boolean medications: type: object properties: list: type: boolean compliance: type: boolean glp1: type: boolean sideEffects: type: boolean mood: type: boolean bmi: type: boolean cycle: type: boolean locale: type: string enum: - de - en - fr - es - it - pl practiceName: type: string maxLength: 120 includeCharts: type: boolean includeAiSummary: type: boolean germanAtc: type: boolean required: - format additionalProperties: false responses: "200": description: "Export generated. Content-Type varies by `format`: application/pdf, application/fhir+json, or application/zip." content: application/pdf: schema: type: string format: binary application/fhir+json: schema: type: string format: binary application/zip: schema: type: string format: binary "401": *a1 "422": *a3 "429": *a4 /api/share-links: post: tags: - Export summary: Create a clinician share link (v1.11.0) description: "Owner-only. Mints an `hls_` token (192-bit), stores only its HMAC hash, and returns the raw token EXACTLY ONCE in the response. Every scope column (window, sections, FHIR resource types, API toggle) is frozen write-once. `expiresAt` is required and capped at 90 days. Auth via cookie or Bearer; rate-limited (`share-link:`, 20/h). Strict: unknown keys 422." requestBody: required: true content: application/json: schema: type: object properties: label: type: string minLength: 1 maxLength: 120 rangeStart: 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)))$ rangeEnd: 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" sections: type: object properties: vitals: type: object properties: weight: type: boolean bp: type: boolean pulse: type: boolean oxygenSaturation: type: boolean bodyFat: type: boolean bodyComposition: type: boolean cardioFitness: type: object properties: restingHeartRate: type: boolean hrv: type: boolean vo2max: type: boolean activity: type: object properties: steps: type: boolean distance: type: boolean energy: type: boolean sleep: type: boolean glucose: type: boolean medications: type: object properties: list: type: boolean compliance: type: boolean glp1: type: boolean sideEffects: type: boolean mood: type: boolean bmi: type: boolean cycle: type: boolean resourceTypes: maxItems: 8 type: array items: type: string enum: - Patient - Observation - MedicationStatement - MedicationAdministration allowFhirApi: type: boolean expiresAt: 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: - label - rangeStart - expiresAt additionalProperties: false responses: "201": description: Share link created. `token` carries the raw `hls_` value and is unrecoverable after this response. content: application/json: schema: $ref: "#/components/schemas/ShareLinkCreated" "401": *a1 "422": *a3 "429": *a4 get: tags: - Export summary: List own clinician share links (v1.11.0) description: Owner-only. Returns the caller's own share links (never the raw token — it is unrecoverable after creation). Auth via cookie or Bearer. responses: "200": description: Share links owned by the caller. content: application/json: schema: $ref: "#/components/schemas/ShareLinkList" "401": *a1 "422": *a3 "429": *a4 /api/share-links/{id}: delete: tags: - Export summary: Revoke a clinician share link (v1.11.0) description: Owner-only. Sets `revokedAt` on the caller's own link. A cross-user or unknown id is sealed as 404. Auth via cookie or Bearer; rate-limited. parameters: - in: path name: id schema: type: string required: true responses: "200": description: Link revoked. content: application/json: schema: $ref: "#/components/schemas/ShareLinkRevoked" "401": *a1 "404": description: Link not found (or owned by another user). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/fhir/metadata: get: tags: - FHIR summary: FHIR R4 CapabilityStatement (v1.11.0) description: "Read-only FHIR R4 capability statement for the REST face. Declares the served resource types (Patient, Observation, MedicationStatement, MedicationAdministration), the `$everything` operation, and the `application/fhir+json` format. Auth: `fhir:read` scope (cookie sessions also pass)." responses: "200": description: CapabilityStatement (application/fhir+json). content: application/fhir+json: schema: type: string format: binary "401": *a1 "422": *a3 "429": *a4 /api/fhir/Patient: get: tags: - FHIR summary: FHIR R4 Patient search (v1.11.0) description: "Read-only `searchset` Bundle of the caller's own Patient resource. Auth: `fhir:read` scope. Offset paging via `_count` (clamped ≤200) / `_offset`. `userId` is narrowed from auth." parameters: - in: query name: _count schema: type: number - in: query name: _offset schema: type: number responses: "200": description: searchset Bundle (application/fhir+json). content: application/fhir+json: schema: type: string format: binary "401": *a1 "422": *a3 "429": *a4 /api/fhir/Observation: get: tags: - FHIR summary: FHIR R4 Observation search (v1.11.0) description: "Read-only `searchset` Bundle of the caller's own Observations (vitals / activity / lab / survey). Auth: `fhir:read` scope. Offset paging via `_count` (clamped ≤200) / `_offset`." parameters: - in: query name: _count schema: type: number - in: query name: _offset schema: type: number responses: "200": description: searchset Bundle (application/fhir+json). content: application/fhir+json: schema: type: string format: binary "401": *a1 "422": *a3 "429": *a4 /api/fhir/MedicationStatement: get: tags: - FHIR summary: FHIR R4 MedicationStatement search (v1.11.0) description: "Read-only `searchset` Bundle of the caller's own active-medication statements. Auth: `fhir:read` scope. Offset paging via `_count` (≤200) / `_offset`." parameters: - in: query name: _count schema: type: number - in: query name: _offset schema: type: number responses: "200": description: searchset Bundle (application/fhir+json). content: application/fhir+json: schema: type: string format: binary "401": *a1 "422": *a3 "429": *a4 /api/fhir/MedicationAdministration: get: tags: - FHIR summary: FHIR R4 MedicationAdministration search (v1.11.0) description: "Read-only `searchset` Bundle of the caller's own acted intakes (completed / not-done). Auth: `fhir:read` scope. Offset paging via `_count` (≤200) / `_offset`." parameters: - in: query name: _count schema: type: number - in: query name: _offset schema: type: number responses: "200": description: searchset Bundle (application/fhir+json). content: application/fhir+json: schema: type: string format: binary "401": *a1 "422": *a3 "429": *a4 /api/fhir/$everything: get: tags: - FHIR summary: FHIR R4 $everything (v1.11.0) description: "Read-only `$everything` operation: every resource in the caller's own record (Patient, Coverage, Observations, MedicationStatements, MedicationAdministrations) in one `searchset` Bundle. Auth: `fhir:read` scope. Offset paging via `_count` (≤200) / `_offset`." parameters: - in: query name: _count schema: type: number - in: query name: _offset schema: type: number responses: "200": description: searchset Bundle (application/fhir+json). content: application/fhir+json: schema: type: string format: binary "401": *a1 "422": *a3 "429": *a4 /api/auth/login: post: tags: - Auth summary: Email-or-username login (password) description: "Browser callers receive a session cookie. Native callers (X-Client-Type: native or HealthLog-iOS UA prefix) additionally receive a paired access + refresh token." requestBody: required: true content: application/json: schema: type: object properties: email: type: string minLength: 1 password: type: string minLength: 1 required: - email - password responses: "200": description: Login succeeded. content: application/json: schema: $ref: "#/components/schemas/LoginResponse" "401": *a1 "422": *a3 "429": *a4 /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": *a3 "429": *a4 /api/auth/refresh: post: tags: - Auth summary: Rotate refresh token (one-time use) description: >- Reuse of a consumed refresh token revokes every refresh token still active for the originating device (per-device blast radius from v1.4.23). Legacy tokens issued before v1.4.23 with a null deviceId fall back to revoke-all-for-user. On a 401, `meta.errorCode` is a stable machine code so the client can branch terminal re-auth from a transient blip without parsing the prose `error`: `auth.refresh.reuse` (a consumed token was replayed — device family revoked, re-pair required), `auth.refresh.revoked` (family revoked out-of-band — re-pair required), `auth.refresh.invalid` (not found / expired — drop the token and re-authenticate). requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RefreshTokenRequest" responses: "200": description: Rotation succeeded — new pair issued. content: application/json: schema: $ref: "#/components/schemas/RefreshResponse" "401": *a1 "422": *a3 "429": *a4 /api/sync/state: get: tags: - Sync summary: Sync handshake + window metadata description: Cheap 'should I sync?' summary. Returns the previous `lastSyncedAt` checkpoint and advances it server-side on each call. The `sync` block carries the incremental-delta window + tombstone retention so the client reads them rather than hardcoding. responses: "200": description: Sync state summary. content: application/json: schema: $ref: "#/components/schemas/SyncStateEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/sync/changes: get: tags: - Sync summary: Measurements delta feed description: "Incremental catch-up after the first-pair backfill (never a replacement for it). Pages over an opaque keyset cursor; each page carries `tombstones` (soft-deleted rows, keyed on `externalId`) and `upserts` (live rows). Apply tombstones before upserts within a page. `cursorExpired: true` forces a clean re-init." parameters: - in: query name: cursor schema: description: Opaque multi-domain keyset cursor from the previous page. Treat as fully opaque — echo, never parse. Omit for the initial sync. type: string minLength: 1 maxLength: 2048 description: Opaque multi-domain keyset cursor from the previous page. Treat as fully opaque — echo, never parse. Omit for the initial sync. - in: query name: limit schema: description: Page size, default 200, hard cap 500. type: integer minimum: 1 maximum: 500 description: Page size, default 200, hard cap 500. responses: "200": description: Delta page. content: application/json: schema: $ref: "#/components/schemas/SyncChangesEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/measurements/series: get: tags: - Measurements summary: Per-kind time series (iOS chart source) description: Maps a camelCase `kind` to the canonical MeasurementType(s) and returns an ordered point series with an explicit `unit` token and a summary `stats` block. `bloodPressure` pairs systolic + diastolic (`secondary`) within ±5 min so one fetch renders the dual-line chart; `sleep` collapses per-stage rows into one per-night point carrying time-asleep in hours (`sleepStages` holds the per-stage breakdown) and is internally capped to 365 days. `glucose` values + `unit` resolve to the user's mg/dL|mmol/L preference. Auth via cookie or Bearer. parameters: - in: query name: kind schema: type: string enum: - weight - bloodPressure - pulse - bodyFat - glucose - sleep - steps - totalBodyWater - boneMass - oxygenSaturation - restingHeartRate - heartRateVariability - vo2Max required: true - in: query name: days schema: default: 30 description: Look-back window in days (1..3650, default 30). `sleep` is internally capped to 365 regardless of the requested range. type: integer minimum: 1 maximum: 3650 description: Look-back window in days (1..3650, default 30). `sleep` is internally capped to 365 regardless of the requested range. responses: "200": description: Resolved series. content: application/json: schema: $ref: "#/components/schemas/GetMeasurementsSeriesResponse" "401": *a1 "422": *a3 "429": *a4 /api/measurements: get: tags: - Measurements summary: List measurements description: Filter by type + date range. Response is paged via offset/limit; default limit 100, hard cap 500. parameters: - in: query name: type schema: type: string enum: - WEIGHT - BLOOD_PRESSURE_SYS - BLOOD_PRESSURE_DIA - PULSE - BODY_FAT - SLEEP_DURATION - ACTIVITY_STEPS - BLOOD_GLUCOSE - TOTAL_BODY_WATER - BONE_MASS - OXYGEN_SATURATION - HEART_RATE_VARIABILITY - RESTING_HEART_RATE - ACTIVE_ENERGY_BURNED - FLIGHTS_CLIMBED - WALKING_RUNNING_DISTANCE - VO2_MAX - BODY_TEMPERATURE - FAT_FREE_MASS - FAT_MASS - MUSCLE_MASS - SKIN_TEMPERATURE - PULSE_WAVE_VELOCITY - VASCULAR_AGE - VISCERAL_FAT - AUDIO_EXPOSURE_ENV - AUDIO_EXPOSURE_HEADPHONE - TIME_IN_DAYLIGHT - WALKING_STEADINESS - AUDIO_EXPOSURE_EVENT - RESPIRATORY_RATE - BODY_MASS_INDEX - LEAN_BODY_MASS - WALKING_HEART_RATE_AVERAGE - WALKING_ASYMMETRY - WALKING_DOUBLE_SUPPORT - WALKING_STEP_LENGTH - WALKING_SPEED - CARDIO_RECOVERY - WRIST_TEMPERATURE - FALL_COUNT - SIX_MINUTE_WALK_DISTANCE - STAIR_ASCENT_SPEED - STAIR_DESCENT_SPEED - BREATHING_DISTURBANCES - IRREGULAR_RHYTHM_NOTIFICATION - HIGH_HEART_RATE_EVENT - LOW_HEART_RATE_EVENT - WALKING_STEADINESS_EVENT - BREATHING_DISTURBANCE_EVENT - RECOVERY_SCORE - STRESS_SCORE - STRAIN_SCORE - HRV_RMSSD - DAY_STRAIN - WORKOUT_STRAIN - SLEEP_PERFORMANCE - SLEEP_EFFICIENCY - SLEEP_CONSISTENCY - SLEEP_NEED - ENERGY_EXPENDITURE_KJ - AVERAGE_HEART_RATE - MAX_HEART_RATE - SLEEP_DISTURBANCE_COUNT - 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: sourceEq schema: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - 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": *a3 "429": *a4 post: tags: - Measurements summary: Create one measurement description: Single ingest. Use `/api/measurements/batch` for Apple Health upload streams. requestBody: required: true content: application/json: schema: type: object properties: type: type: string enum: - WEIGHT - BLOOD_PRESSURE_SYS - BLOOD_PRESSURE_DIA - PULSE - BODY_FAT - SLEEP_DURATION - ACTIVITY_STEPS - BLOOD_GLUCOSE - TOTAL_BODY_WATER - BONE_MASS - OXYGEN_SATURATION - HEART_RATE_VARIABILITY - RESTING_HEART_RATE - ACTIVE_ENERGY_BURNED - FLIGHTS_CLIMBED - WALKING_RUNNING_DISTANCE - VO2_MAX - BODY_TEMPERATURE - FAT_FREE_MASS - FAT_MASS - MUSCLE_MASS - SKIN_TEMPERATURE - PULSE_WAVE_VELOCITY - VASCULAR_AGE - VISCERAL_FAT - AUDIO_EXPOSURE_ENV - AUDIO_EXPOSURE_HEADPHONE - TIME_IN_DAYLIGHT - WALKING_STEADINESS - AUDIO_EXPOSURE_EVENT - RESPIRATORY_RATE - BODY_MASS_INDEX - LEAN_BODY_MASS - WALKING_HEART_RATE_AVERAGE - WALKING_ASYMMETRY - WALKING_DOUBLE_SUPPORT - WALKING_STEP_LENGTH - WALKING_SPEED - CARDIO_RECOVERY - WRIST_TEMPERATURE - FALL_COUNT - SIX_MINUTE_WALK_DISTANCE - STAIR_ASCENT_SPEED - STAIR_DESCENT_SPEED - BREATHING_DISTURBANCES - IRREGULAR_RHYTHM_NOTIFICATION - HIGH_HEART_RATE_EVENT - LOW_HEART_RATE_EVENT - WALKING_STEADINESS_EVENT - BREATHING_DISTURBANCE_EVENT - RECOVERY_SCORE - STRESS_SCORE - STRAIN_SCORE - HRV_RMSSD - DAY_STRAIN - WORKOUT_STRAIN - SLEEP_PERFORMANCE - SLEEP_EFFICIENCY - SLEEP_CONSISTENCY - SLEEP_NEED - ENERGY_EXPENDITURE_KJ - AVERAGE_HEART_RATE - MAX_HEART_RATE - SLEEP_DISTURBANCE_COUNT value: type: number measuredAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ notes: type: string maxLength: 200 source: default: MANUAL type: string enum: - MANUAL - 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": *a3 "429": *a4 /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": *a3 "429": *a4 /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": *a3 "429": *a4 /api/measurements/bulk-delete: post: tags: - Measurements summary: Bulk soft-delete measurements description: Soft-deletes (tombstones) up to 200 of the caller's measurement rows in one call, mirroring the single-DELETE contract. Idempotent via the `Idempotency-Key` header; rate-limited `measurements:bulk-delete:`. Forged / foreign / already-deleted ids are silently skipped — `deleted` is the count of rows actually tombstoned. The deletions surface in `/api/sync/changes` as tombstones. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MeasurementsBulkDeleteRequest" responses: "200": description: Bulk delete processed. content: application/json: schema: $ref: "#/components/schemas/MeasurementsBulkDeleteResponse" "401": *a1 "422": *a3 "429": *a4 /api/measurements/restore: post: tags: - Measurements summary: Restore soft-deleted measurements description: Un-tombstones up to 200 of the caller's soft-deleted measurement rows in one call — the delete-Undo counterpart to `/api/measurements/bulk-delete`. Idempotent via the `Idempotency-Key` header; rate-limited `measurements:restore:`. Forged / foreign / not-deleted ids are silently skipped — `restored` is the count of rows actually un-tombstoned. The restored rows re-surface in `/api/sync/changes` as upserts. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MeasurementsRestoreRequest" responses: "200": description: Restore processed. content: application/json: schema: $ref: "#/components/schemas/MeasurementsRestoreResponse" "401": *a1 "422": *a3 "429": *a4 /api/sleep/night: get: tags: - Measurements summary: Reconstructed sleep night (hypnogram source, v1.11.5) description: "Returns one night's reconstructed sleep session for the phase-progression (hypnogram) view: the canonical source's stage segments (each with an absolute start/end), the per-stage breakdown, asleep/in-bed/awake totals, the mid-sleep awakenings count, and same-wake-day naps surfaced separately. Session-clustered, keyed by the local wake day, collapsed to one source via the sleep priority ladder so two sources never overlay. `date` omitted returns the most recent night. A read-only view over existing per-stage SLEEP_DURATION rows — no schema, no new measurement type." parameters: - in: query name: date schema: description: Wake-day key (YYYY-MM-DD). Omit for the most recent night. type: string pattern: ^\d{4}-\d{2}-\d{2}$ description: Wake-day key (YYYY-MM-DD). Omit for the most recent night. responses: "200": description: Reconstructed night. content: application/json: schema: $ref: "#/components/schemas/SleepNightResponse" "401": *a1 "422": *a3 "429": *a4 /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": *a3 "429": *a4 /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": *a3 "429": *a4 /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 - COMPUTED - WHOOP - FITBIT 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 samples: minItems: 1 maxItems: 30000 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)))$ hr: type: integer minimum: 20 maximum: 300 speedMs: type: number minimum: 0 maximum: 50 power: type: number minimum: 0 maximum: 3000 cadence: type: number minimum: 0 maximum: 400 required: - t 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": *a3 "429": *a4 /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": *a3 "429": *a4 /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 today's actioned intake events (`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": *a3 "429": *a4 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": *a3 "429": *a4 /api/medications/layout: get: tags: - Medications summary: Read the calling user's medications list presentation description: Returns the per-user /medications presentation (card/table view + manual order). Falls back to the defaults (cards, empty order) when the user has not customised it. Mirrors the insights-layout contract. responses: "200": description: The resolved presentation (custom or default). content: application/json: schema: $ref: "#/components/schemas/MedicationListLayoutResponse" "401": *a1 "422": *a3 "429": *a4 put: tags: - Medications summary: Update the calling user's medications list presentation description: "Field-scoped update: `view` and `order` are each optional, and whichever the body omits is preserved from the stored blob — a view toggle can never wipe the manual order and vice versa. The normalised presentation is returned. Invalid bodies return the multi-issue 422 envelope." requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MedicationListLayout" responses: "200": description: Presentation saved; the normalised blob is echoed back. content: application/json: schema: $ref: "#/components/schemas/MedicationListLayoutSaved" "401": *a1 "422": *a3 "429": *a4 delete: tags: - Medications summary: Reset the calling user's medications list presentation description: Clears the persisted presentation and returns the defaults (cards, empty order). Idempotent. responses: "200": description: Presentation reset; the defaults are returned. content: application/json: schema: $ref: "#/components/schemas/MedicationListLayoutReset" "401": *a1 "422": *a3 "429": *a4 /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": *a3 "429": *a4 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": *a3 "429": *a4 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": *a3 "429": *a4 /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": *a3 "429": *a4 /api/medications/{id}/inventory: get: tags: - Medications summary: List a medication's supply containers description: Returns every inventory item (all states) for the medication, ordered by state, then `expiresAt`, then `createdAt`. Items count UNITS; divide by the medication's `unitsPerDose` for dose-level figures. parameters: - in: path name: id schema: type: string required: true responses: "200": description: Inventory item list. content: application/json: schema: $ref: "#/components/schemas/ListMedicationInventoryResponse" "401": *a1 "404": description: Medication not found (or owned by another user). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 post: tags: - Medications summary: Register a new supply container description: Creates an ACTIVE inventory item with `unitsRemaining = unitsTotal`. The request's `unitsTotal` field carries UNITS (1–1000). Rate-limited 30/min/user. Audits as `medication.inventory.create`. parameters: - in: path name: id schema: type: string required: true requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateMedicationInventoryItemRequest" responses: "201": description: Created inventory item. content: application/json: schema: $ref: "#/components/schemas/CreateMedicationInventoryItemResponse" "401": *a1 "404": description: Medication not found (or owned by another user). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/medications/{id}/inventory/{itemId}: patch: tags: - Medications summary: Mutate a supply container description: "Per-item operations: manual first-use (`markAsFirstUseAt`), used-up override (`markAsUsedUp`), printed-expiry correction, absolute remaining-unit correction (`unitsRemaining`, clamped to the item's capacity), notes. The canonical state machine re-derives the state after every mutation. Audits as `medication.inventory.update`." parameters: - in: path name: id schema: type: string required: true - in: path name: itemId schema: type: string required: true requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateMedicationInventoryItemRequest" responses: "200": description: Updated inventory item. content: application/json: schema: $ref: "#/components/schemas/UpdateMedicationInventoryItemResponse" "401": *a1 "404": description: Inventory item not found (or owned by another user / medication). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 delete: tags: - Medications summary: Delete a supply container description: Hard-deletes the inventory item. The audit log captures the before-state (`medication.inventory.delete`) so a row can be reconstructed if needed. Consumption stamps on intake events that reference the item stay in place; a later restore skips the missing container. parameters: - in: path name: id schema: type: string required: true - in: path name: itemId schema: type: string required: true responses: "200": description: Deletion succeeded. content: application/json: schema: $ref: "#/components/schemas/DeleteMedicationInventoryItemResponse" "401": *a1 "404": description: Inventory item not found (or owned by another user / medication). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/medications/extract: post: tags: - Medications summary: Extract scheduling fields from a free-text medication description description: Runs the user's free-text description through the Coach provider chain and returns a citation-guarded partial payload the wizard merges onto whatever the user already typed. `name` and `dose` are dropped when not substring-matched in the original text so the wizard cannot land a hallucinated brand or dose. `cadenceKind` / `doseUnit` / `weekdays` are closed enums; numeric fields are clamped. Rate-limited 10 requests / 5 minutes / user, gated against the daily Coach token budget. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MedicationExtractRequest" responses: "200": description: Citation-guarded partial extraction. content: application/json: schema: $ref: "#/components/schemas/MedicationExtractResponse" "401": *a1 "422": *a3 "429": *a4 "502": description: Upstream provider returned an empty, unparseable, or off-schema reply. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "503": description: No AI provider configured for the calling user (or operator). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" /api/medications/{id}/cadence: get: tags: - Medications summary: Cadence + compliance read for a medication description: Returns the expected-vs-actual dose timeline for the requested window plus the four compliance chip values that drive the detail-page section. Pure computation — no writes. Day boundaries are resolved in the user's IANA timezone so a Tokyo user and a Berlin user see the same chips for the same medication. The `days` query parameter caps at 180. parameters: - in: path name: id schema: type: string required: true - in: query name: days schema: description: Window size in days (default 30, max 180). type: integer minimum: 1 maximum: 180 description: Window size in days (default 30, max 180). responses: "200": description: Cadence response. content: application/json: schema: $ref: "#/components/schemas/GetMedicationCadenceResponse" "401": *a1 "404": description: Medication not found (or owned by another user). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/medications/compliance: get: tags: - Medications summary: Batched adherence read for every medication of the caller description: Returns one compact adherence row per medication the caller owns (active + paused), ordered by `createdAt DESC` — the single round trip the medication cards consume instead of fanning out one `/api/medications/{id}/compliance` request per card. Each row carries the 7-/30-day summaries and the cadence-scaled display block; the per-day grid stays on the per-medication endpoint. Pure computation — no writes. Served through the same per-medication server cache as the per-id read, so the two endpoints warm each other. responses: "200": description: One adherence row per medication. content: application/json: schema: $ref: "#/components/schemas/ListMedicationComplianceResponse" "401": *a1 "422": *a3 "429": *a4 /api/medications/{id}/compliance: get: tags: - Medications summary: Adherence read for a medication description: Returns the 7- and 30-day adherence summaries, the per-day compliance grid for the history glyph track, and the two-row display block. Pure computation — no writes. Day boundaries are resolved in the user's IANA timezone, and the expected-dose denominator is cadence-aware (RRULE / rolling / one-shot / PRN / cyclic) and clamped to the medication's `createdAt`. Read `compliance30` for the headline 30-day taken-vs-expected percentage; build the per-day glyph track from `dailyCompliance` (draw a cell only where `due === true`). parameters: - in: path name: id schema: type: string required: true responses: "200": description: Compliance response. content: application/json: schema: $ref: "#/components/schemas/GetMedicationComplianceResponse" "401": *a1 "404": description: Medication not found (or owned by another user). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/medications/{id}/schedule-revisions: get: tags: - Medications summary: List a medication's archived schedule eras description: Returns every archived schedule era (newest first) plus `currentSince`, the instant the live plan took over. The dose-history ledger and compliance tallies already mint past days against these eras; this read powers the Zeitplan-tab history timeline. parameters: - in: path name: id schema: type: string required: true responses: "200": description: Era list. content: application/json: schema: $ref: "#/components/schemas/ListScheduleRevisionsResponse" "401": *a1 "404": description: Medication not found (or owned by another user). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 post: tags: - Medications summary: Append a manual schedule era (pre-tracking history) description: Records that the medication dosed at the given daily times during `[validFrom, validUntil)` — history from before the schedule was edited in the app. The era must end at or before the start of the live plan and must not overlap an existing era; violations return 422. The snapshot is shaped exactly like a write-path archive (`FREQ=DAILY`, window pulled to the min/max of the times), so every historical surface reads it transparently. Audits as `medication.schedule_revision.created`. parameters: - in: path name: id schema: type: string required: true requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateScheduleRevisionRequest" responses: "201": description: Manual era created. content: application/json: schema: $ref: "#/components/schemas/CreateScheduleRevisionResponse" "401": *a1 "404": description: Medication not found (or owned by another user). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/medications/{id}/schedule-revisions/{revisionId}: patch: tags: - Medications summary: Correct a recorded schedule era description: "Replaces an era's bounds and daily times. A `MANUAL` era updates in place; an `ARCHIVED` era stays as the immutable audit record and the correction is minted as a superseding `MANUAL` revision that takes its place in every historical surface (the response carries the correction's id). Validation mirrors the sibling POST: the era must end at or before the start of the live plan and must not overlap another active era; violations return 422. An era that has already been corrected refuses with 409. Audits as `medication.schedule_revision.updated`." parameters: - in: path name: id schema: type: string required: true - in: path name: revisionId schema: type: string required: true requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateScheduleRevisionRequest" responses: "200": description: Era corrected. content: application/json: schema: $ref: "#/components/schemas/UpdateScheduleRevisionResponse" "401": *a1 "404": description: Medication or revision not found (or owned by another user). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "409": description: The revision has already been superseded by a correction. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 delete: tags: - Medications summary: Delete a manually added schedule era description: "Removes a `MANUAL` era — one appended through the sibling POST, or a correction minted by PATCH (deleting a correction restores the archived original it superseded). Write-path archives (`source: ARCHIVED`) are immutable history and refuse with 409. Audits as `medication.schedule_revision.deleted`." parameters: - in: path name: id schema: type: string required: true - in: path name: revisionId schema: type: string required: true responses: "200": description: Deletion succeeded. content: application/json: schema: $ref: "#/components/schemas/DeleteScheduleRevisionResponse" "401": *a1 "404": description: Medication or revision not found (or owned by another user). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "409": description: The revision is a write-path archive (`ARCHIVED`) and cannot be deleted. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/user/ai-provider: get: tags: - Auth summary: Read the calling user's AI-provider configuration description: "Returns the user's selected provider/model/baseUrl plus the effective availability: `aiAvailable` is true when any provider can serve the user (BYOK, self-hosted, or the operator's server-managed key), and `managedBy` reports which. iOS gates Coach visibility off these two fields. Key material is reported as presence + a 4-char masked preview only." responses: "200": description: Resolved AI-provider configuration. content: application/json: schema: $ref: "#/components/schemas/GetAiProviderResponse" "401": *a1 "422": *a3 "429": *a4 /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": *a3 "429": *a4 put: tags: - Auth summary: Replace per-user Coach prompt-tuning preferences description: Persists the supplied prefs. Body is validated against `CoachPrefs`; missing keys fall back to the documented defaults. requestBody: required: true content: application/json: schema: type: object properties: tone: default: warm type: string enum: - warm - neutral - concise verbosity: default: default type: string enum: - brief - default - detailed excludeMetrics: default: [] maxItems: 11 type: array items: type: string enum: - bp - weight - pulse - mood - compliance - hrv - sleep - resting_hr - steps - medications - anthropometrics showEvidenceByDefault: default: false type: boolean defaultWindow: default: allTime type: string enum: - last7days - last30days - last90days - allTime dataClusters: maxItems: 10 type: array items: type: string enum: - cardio - body - activity - workouts - sleep - mood - glucose - medication - mobility - environment responses: "200": description: Saved preferences echoed back. content: application/json: schema: $ref: "#/components/schemas/PutCoachPrefsResponse" "401": *a1 "422": *a3 "429": *a4 /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": *a3 "429": *a4 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": *a3 "429": *a4 /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": *a3 "429": *a4 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 - COMPUTED - WHOOP - FITBIT activeEnergy: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT walkingRunningDistance: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT flightsClimbed: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT sleep: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT weight: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT bloodPressure: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT pulse: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT bodyFat: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT bodyTemperature: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT spo2: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT hrv: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT restingHeartRate: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT vo2Max: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT skinTemperature: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT respiratoryRate: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT recovery: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT metricPriority: type: object properties: steps: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT activeEnergy: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT walkingRunningDistance: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT flightsClimbed: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT sleep: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT weight: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT bloodPressure: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT pulse: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT bodyFat: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT bodyTemperature: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT spo2: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT hrv: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT restingHeartRate: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT vo2Max: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT skinTemperature: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT respiratoryRate: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT recovery: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT 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": *a3 "429": *a4 /api/user/avatar: post: tags: - Auth summary: Upload the calling user's avatar description: "Multipart upload (field `file`). Accepts image/jpeg, image/png, image/webp; rejects anything else at the magic-byte sniff (the multipart Content-Type header is informational only). Hard caps: 2 MiB body, 2048×2048 dimensions. Replaces the v1.4.22 Gravatar leak by storing the bytes on the User row + serving them from same-origin." requestBody: required: true content: multipart/form-data: schema: type: object properties: file: type: string description: Binary image payload (JPEG / PNG / WebP). Hard-capped at 2 MiB and 2048×2048. required: - file responses: "201": description: Avatar saved. `avatarUrl` is the same value the /me payload returns; the `?v=` suffix busts client caches on re-upload. content: application/json: schema: $ref: "#/components/schemas/UploadAvatarResponse" "401": *a1 "413": description: Upload exceeds the 2 MiB byte limit or the 2048×2048 dimension limit. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "415": description: Image format is not one of JPEG / PNG / WebP (magic-byte sniff). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 delete: tags: - Auth summary: Clear the calling user's avatar description: Resets the avatar columns to null. Idempotent — a delete on an already-empty row returns 204 with no audit-log row. responses: "204": description: Avatar cleared (or already empty). "401": *a1 "422": *a3 "429": *a4 /api/user/avatar/{id}: get: tags: - Auth summary: Serve a user's avatar bytes description: Owner-scoped. The `{id}` path segment must match the calling user's id — cross-user reads return 403. Response body is the raw image bytes with the persisted `Content-Type`; the /me payload appends `?v={updatedAtMs}` so clients can cache aggressively. parameters: - in: path name: id schema: type: string description: User id (must match the calling user). required: true description: User id (must match the calling user). responses: "200": description: Avatar bytes. content: image/jpeg: schema: type: string description: Raw JPEG bytes. image/png: schema: type: string description: Raw PNG bytes. image/webp: schema: type: string description: Raw WebP bytes. "401": *a1 "403": description: Caller is not the avatar's owner. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "404": description: The user has no uploaded avatar. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/user/profile: get: tags: - Auth summary: Read the calling user's profile description: Flattened profile fields for the native client. Aliased over the same data exposed by /api/auth/me. The KVNR is decrypted server-side; the IKNR (v1.8.6) is returned plaintext. responses: "200": description: The calling user's profile. content: application/json: schema: $ref: "#/components/schemas/GetProfileResponse" "401": *a1 "422": *a3 "429": *a4 patch: tags: - Auth summary: Update the calling user's profile description: Partial profile update — every field is optional. An explicit null (or empty string) clears a field. A non-empty `insurerIkNumber` must be exactly 9 digits (422 otherwise); a non-empty `insuranceNumber` (KVNR) must pass the mod-10 check. `userId` is never accepted from the body. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ProfileUpdateRequest" responses: "200": description: Saved profile echoed back. content: application/json: schema: $ref: "#/components/schemas/PatchProfileResponse" "401": *a1 "422": *a3 "429": *a4 /api/auth/me/notification-prefs: get: tags: - Notifications summary: Read per-user notification preferences description: Returns the fully-resolved preferences with the documented defaults filled in. A null persisted row resolves to the defaults (server-managed reminders, low-stock threshold 7 days, mood reminder 22:00, Coach nudges on). responses: "200": description: Resolved preferences. content: application/json: schema: $ref: "#/components/schemas/GetNotificationPrefsResponse" "401": *a1 "422": *a3 "429": *a4 patch: tags: - Notifications summary: Update per-user notification preferences description: 'Deep-merges the supplied partial shape over the persisted row — a PATCH touching only one category leaves the siblings intact. Always returns the fully-resolved next state so clients can hard-set their optimistic update. `medication.clientManaged: true` (or `deliveryDefault: "client"`) suppresses server-side MEDICATION_REMINDER APNs; `medication.lowStockRunwayDays` (1–60, nullable, default 7) tunes the low-stock alert — null switches it off. Rate-limit 60/min per user.' requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/NotificationPrefsPatchRequest" responses: "200": description: Resolved next-state preferences echoed back. content: application/json: schema: $ref: "#/components/schemas/PatchNotificationPrefsResponse" "401": *a1 "422": *a3 "429": *a4 /api/insights/layout: get: tags: - Insights summary: Read the calling user's Insights tile layout description: Returns the per-user Insights tile layout (visibility + order). Falls back to the default layout when the user has not customised it. Mirrors the dashboard-widgets contract. responses: "200": description: The resolved layout (custom or default). content: application/json: schema: $ref: "#/components/schemas/InsightsLayout" "401": *a1 "422": *a3 "429": *a4 put: tags: - Insights summary: Replace the calling user's Insights tile layout description: Persists the full tile layout. The normalised layout is returned. Invalid bodies return the multi-issue 422 envelope, matching the dashboard-widgets route. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/InsightsLayoutBody" responses: "200": description: Layout saved; the normalised layout is echoed back. content: application/json: schema: $ref: "#/components/schemas/InsightsLayoutSaved" "401": *a1 "422": *a3 "429": *a4 delete: tags: - Insights summary: Reset the calling user's Insights tile layout description: Clears the persisted layout and returns the default layout. Idempotent. responses: "200": description: Layout reset; the default layout is returned. content: application/json: schema: $ref: "#/components/schemas/InsightsLayoutReset" "401": *a1 "422": *a3 "429": *a4 /api/insights/coach/facts: get: tags: - Insights summary: List the caller's durable Coach facts description: v1.11.1 — returns the active facts the Coach has extracted about the caller (highest-confidence then newest first), each decrypted on the fly. The GDPR 'what do you know about me' surface. Coach-gated (`requireAssistantSurface("coach")`). Auth via cookie or Bearer; the owner is always narrowed from the session, never the body. Undecryptable rows are omitted rather than failing the read. responses: "200": description: The caller's active facts. content: application/json: schema: $ref: "#/components/schemas/CoachFactsList" "401": *a1 "422": *a3 "429": *a4 delete: tags: - Insights summary: Forget all of the caller's Coach facts description: "v1.11.1 — bulk 'forget what you know about me': soft-deletes every active fact for the caller and returns the count cleared. Idempotent (a second call clears 0). Coach-gated. Auth via cookie or Bearer." responses: "200": description: All active facts cleared; the count is returned. content: application/json: schema: $ref: "#/components/schemas/CoachFactsCleared" "401": *a1 "422": *a3 "429": *a4 /api/insights/coach/facts/{id}: delete: tags: - Insights summary: Forget one Coach fact description: "v1.11.1 — soft-deletes a single fact owned by the caller. An unknown / cross-user / already-deleted id is an idempotent no-op returning `{ deleted: false }`, never revealing whether the id exists under another account. Coach-gated. Auth via cookie or Bearer." responses: "200": description: "The fact was soft-deleted (`deleted: true`) or the id matched nothing the caller owns (`deleted: false`)." content: application/json: schema: $ref: "#/components/schemas/CoachFactDeleted" "401": *a1 "422": *a3 "429": *a4 /api/coach/about-me: get: tags: - Insights summary: Read the caller's self-context description: v1.16.0 — returns the structured self-context (free text plus chronic conditions, allergies, coach focus) the Coach system prompt and the daily briefing inject as a delimited, user-provided context block, alongside any pending clarifying questions. Every field is stored encrypted at rest; an undecryptable payload reads as null (fail closed). Auth via cookie or Bearer; the owner is always narrowed from the session. responses: "200": description: The stored fields (null when never written / cleared) plus pending clarifying questions. content: application/json: schema: $ref: "#/components/schemas/GetCoachAboutMeResponse" "401": *a1 "422": *a3 "429": *a4 put: tags: - Insights summary: Write (or clear) the caller's self-context description: "v1.16.0 — persists the free text (4 000-char cap) and the three structured fields (500-char cap each) encrypted at rest; caps are enforced before encryption. Structured fields are optional: omitted leaves the stored value untouched, an empty string clears it. After a non-empty save the server derives up to 3 clarifying questions (AI when a provider and the daily Coach token budget allow, deterministic completion hints otherwise) and returns them as `pendingQuestions`. Rate-limited per user." requestBody: required: true content: application/json: schema: type: object properties: aboutMe: type: string maxLength: 4000 conditions: type: string maxLength: 500 allergies: type: string maxLength: 500 coachFocus: type: string maxLength: 500 required: - aboutMe responses: "200": description: The effective (trimmed) state echoed back plus the freshly derived pending questions. content: application/json: schema: $ref: "#/components/schemas/PutCoachAboutMeResponse" "401": *a1 "422": *a3 "429": *a4 /api/coach/about-me/questions: get: tags: - Insights summary: Read the pending clarifying questions description: v1.16.0 — the up-to-3 clarifying questions derived after the last self-context save. The Coach composer renders them as tappable suggestion chips. Stored encrypted; an undecryptable payload reads as an empty list (fail closed). responses: "200": description: The pending questions (possibly empty). content: application/json: schema: $ref: "#/components/schemas/GetCoachAboutMeQuestionsResponse" "401": *a1 "422": *a3 "429": *a4 delete: tags: - Insights summary: Dismiss pending clarifying questions description: v1.16.0 — dismisses one question (body `{ question }`, exact match) or all of them (empty body). Tapping a chip in the Coach composer inserts the question into the chat input and dismisses it here. requestBody: required: false content: application/json: schema: type: object properties: question: description: Exact question text to dismiss. Omitted = dismiss all. type: string responses: "200": description: The remaining questions after the dismissal. content: application/json: schema: $ref: "#/components/schemas/DeleteCoachAboutMeQuestionsResponse" "401": *a1 "422": *a3 "429": *a4 /api/admin/invites: get: tags: - Admin summary: List registration invites description: v1.16.0 — every invite with creator / consumer usernames, use counters, soft-revocation timestamp, and the full per-signup redemption ledger (`redemptions`; `consumer` only carries the LAST account on a multi-use invite). Metadata only — the raw token is never derivable from this endpoint (only its HMAC hash is persisted). Admin session cookie required; Bearer tokens cannot reach admin endpoints. responses: "200": description: All invites, newest first. content: application/json: schema: $ref: "#/components/schemas/AdminInviteList" "401": *a1 "422": *a3 "429": *a4 post: tags: - Admin summary: Mint a registration invite description: v1.15.20 — creates an invite that admits a signup even while open registration is disabled. The raw `hlv_<64hex>` token and the composed registration URL appear EXACTLY ONCE in this response; only the keyed hash is persisted. Lifetime is capped at 30 days; `maxUses` makes multi-use invites possible (consumption is an atomic guarded increment). Admin session cookie required. requestBody: required: true content: application/json: schema: type: object properties: expiresInDays: default: 7 type: integer minimum: 1 maximum: 30 maxUses: default: 1 type: integer minimum: 1 maximum: 50 responses: "201": description: The minted invite, including the one-time raw token. content: application/json: schema: $ref: "#/components/schemas/AdminInviteCreated" "401": *a1 "422": *a3 "429": *a4 /api/admin/invites/{id}: delete: tags: - Admin summary: Revoke a registration invite description: "v1.16.0 — soft-revokes the invite (`revokedAt`): the row keeps its redemption history visible in the admin table while the consume path refuses the link like an expired one. Idempotent: an unknown or already-revoked id returns `{ revoked: false }` instead of 404. Admin session cookie required." parameters: - in: path name: id schema: type: string required: true responses: "200": description: "The invite was revoked (`revoked: true`) or nothing matched (`revoked: false`)." content: application/json: schema: $ref: "#/components/schemas/AdminInviteRevoked" "401": *a1 "422": *a3 "429": *a4 /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": *a3 "429": *a4 /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": *a3 "429": *a4 /api/dashboard/snapshot: get: tags: - Dashboard summary: Unified dashboard first-paint snapshot description: "Assembles every above-the-fold tile field in one round-trip from the rollup / mood / widget helpers plus a read-only lift of the pre-generated daily briefing. Two-phase: `tiles` always present, `extras` nullable on a rollup-coverage miss. No LLM is reachable from this path. Cookie or Bearer auth." responses: "200": description: Dashboard snapshot. content: application/json: schema: $ref: "#/components/schemas/DashboardSnapshotResponseEnvelope" "401": *a1 "422": *a3 "429": *a4 /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. Requires an active ConsentReceipt when the resolved provider chain egresses via the operator's server-managed key (see POST /api/consent/ai). responses: "200": description: Insights bundle. content: application/json: schema: $ref: "#/components/schemas/InsightsComprehensiveResponseEnvelope" "401": *a1 "403": description: "AI consent required: no active ConsentReceipt for the server-managed provider. `meta.errorCode` = `consent.ai.required`. Mint a receipt via POST /api/consent/ai before retrying." content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/insights/pregenerate: post: tags: - Insights summary: Warm all AI assessments for the calling user description: v1.8.7.1 — enqueue a full warm of every AI assessment for the authenticated user (comprehensive insight + the seven specialised status cards + every data-bearing generic metric assessment) in the active locale, so the read-only status GETs serve cached text instantly. Returns immediately; the generation runs out of band on the worker. Empty metrics and provider-less accounts never trigger an LLM call. Short anti-spam bucket (`insights-warm:`, one warm per 3 minutes) → 429 on a tight loop. Auth via cookie or Bearer; `userId` is taken from the session, never the body. requestBody: required: false content: application/json: schema: $ref: "#/components/schemas/InsightsPregenerateRequest" responses: "200": description: Warm accepted and enqueued. The work runs on the worker; poll the read-only status routes for the text. content: application/json: schema: $ref: "#/components/schemas/InsightsPregenerateResponseEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/analytics/range: get: tags: - Analytics summary: Single-metric period-over-period range delta description: v1.9.0 — returns the current-window aggregate, the previous comparable window, and the composed delta for ONE metric type over a `7d` / `30d` / `90d` / `1y` range. Single-type by construction (the metric page is single-metric), so the read is one rollup-tier call covering the trailing 2N days sliced into the two halves — no per-type fan-out. Additive route; the `/api/analytics` envelope is unchanged. Auth via cookie or Bearer. parameters: - in: query name: type schema: type: string enum: - WEIGHT - BLOOD_PRESSURE_SYS - BLOOD_PRESSURE_DIA - PULSE - BODY_FAT - SLEEP_DURATION - ACTIVITY_STEPS - BLOOD_GLUCOSE - TOTAL_BODY_WATER - BONE_MASS - OXYGEN_SATURATION - HEART_RATE_VARIABILITY - RESTING_HEART_RATE - ACTIVE_ENERGY_BURNED - FLIGHTS_CLIMBED - WALKING_RUNNING_DISTANCE - VO2_MAX - BODY_TEMPERATURE - FAT_FREE_MASS - FAT_MASS - MUSCLE_MASS - SKIN_TEMPERATURE - PULSE_WAVE_VELOCITY - VASCULAR_AGE - VISCERAL_FAT - AUDIO_EXPOSURE_ENV - AUDIO_EXPOSURE_HEADPHONE - TIME_IN_DAYLIGHT - WALKING_STEADINESS - AUDIO_EXPOSURE_EVENT - RESPIRATORY_RATE - BODY_MASS_INDEX - LEAN_BODY_MASS - WALKING_HEART_RATE_AVERAGE - WALKING_ASYMMETRY - WALKING_DOUBLE_SUPPORT - WALKING_STEP_LENGTH - WALKING_SPEED - CARDIO_RECOVERY - WRIST_TEMPERATURE - FALL_COUNT - SIX_MINUTE_WALK_DISTANCE - STAIR_ASCENT_SPEED - STAIR_DESCENT_SPEED - BREATHING_DISTURBANCES - IRREGULAR_RHYTHM_NOTIFICATION - HIGH_HEART_RATE_EVENT - LOW_HEART_RATE_EVENT - WALKING_STEADINESS_EVENT - BREATHING_DISTURBANCE_EVENT - RECOVERY_SCORE - STRESS_SCORE - STRAIN_SCORE - HRV_RMSSD - DAY_STRAIN - WORKOUT_STRAIN - SLEEP_PERFORMANCE - SLEEP_EFFICIENCY - SLEEP_CONSISTENCY - SLEEP_NEED - ENERGY_EXPENDITURE_KJ - AVERAGE_HEART_RATE - MAX_HEART_RATE - SLEEP_DISTURBANCE_COUNT description: "The measurement type to read (single metric — no fan-out). Closed enum: an unknown type 422s." required: true description: "The measurement type to read (single metric — no fan-out). Closed enum: an unknown type 422s." - in: query name: range schema: type: string enum: - 7d - 30d - 90d - 1y description: "Trailing window: `7d` / `30d` / `90d` / `1y`. The previous comparable window is the equally-sized span immediately before it." required: true description: "Trailing window: `7d` / `30d` / `90d` / `1y`. The previous comparable window is the equally-sized span immediately before it." responses: "200": description: Current + previous window aggregates and the delta. content: application/json: schema: $ref: "#/components/schemas/AnalyticsRangeResponseEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/insights/blood-pressure-status: get: tags: - Insights summary: Blood-pressure assessment description: "Data-driven plain-language assessment of the user's recent blood-pressure readings. Read-only: a cache miss warms a generation out of band and serves the last-good text meanwhile (stale-while-revalidate). Auth via cookie or Bearer." parameters: - in: query name: locale schema: description: Optional UI-locale override; defaults to the session locale. type: string enum: - de - en description: Optional UI-locale override; defaults to the session locale. responses: "200": description: Assessment envelope (fresh, cached, or preparing). content: application/json: schema: $ref: "#/components/schemas/BloodPressureStatusResponseEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/insights/pulse-status: get: tags: - Insights summary: Pulse assessment description: "Data-driven plain-language assessment of the user's recent resting-pulse readings. Read-only: a cache miss warms a generation out of band and serves the last-good text meanwhile (stale-while-revalidate). Auth via cookie or Bearer." parameters: - in: query name: locale schema: description: Optional UI-locale override; defaults to the session locale. type: string enum: - de - en description: Optional UI-locale override; defaults to the session locale. responses: "200": description: Assessment envelope (fresh, cached, or preparing). content: application/json: schema: $ref: "#/components/schemas/PulseStatusResponseEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/insights/weight-status: get: tags: - Insights summary: Weight assessment description: "Data-driven plain-language assessment of the user's recent weight trend. Read-only: a cache miss warms a generation out of band and serves the last-good text meanwhile (stale-while-revalidate). Auth via cookie or Bearer." parameters: - in: query name: locale schema: description: Optional UI-locale override; defaults to the session locale. type: string enum: - de - en description: Optional UI-locale override; defaults to the session locale. responses: "200": description: Assessment envelope (fresh, cached, or preparing). content: application/json: schema: $ref: "#/components/schemas/WeightStatusResponseEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/insights/bmi-status: get: tags: - Insights summary: BMI assessment description: "Data-driven plain-language assessment of the user's body-mass index. Read-only: a cache miss warms a generation out of band and serves the last-good text meanwhile (stale-while-revalidate). Auth via cookie or Bearer." parameters: - in: query name: locale schema: description: Optional UI-locale override; defaults to the session locale. type: string enum: - de - en description: Optional UI-locale override; defaults to the session locale. responses: "200": description: Assessment envelope (fresh, cached, or preparing). content: application/json: schema: $ref: "#/components/schemas/BmiStatusResponseEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/insights/mood-status: get: tags: - Insights summary: Mood assessment description: "Data-driven plain-language assessment of the user's recent mood entries. Read-only: a cache miss warms a generation out of band and serves the last-good text meanwhile (stale-while-revalidate). Auth via cookie or Bearer." parameters: - in: query name: locale schema: description: Optional UI-locale override; defaults to the session locale. type: string enum: - de - en description: Optional UI-locale override; defaults to the session locale. responses: "200": description: Assessment envelope (fresh, cached, or preparing). content: application/json: schema: $ref: "#/components/schemas/MoodStatusResponseEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/insights/medication-compliance-status: get: tags: - Insights summary: Medication-compliance assessment description: "Data-driven plain-language assessment of the user's medication compliance — an overall `summary` plus a per-medication note array. Read-only: a cache miss warms a generation out of band and serves the last-good envelope meanwhile (stale-while-revalidate). Auth via cookie or Bearer." parameters: - in: query name: locale schema: description: Optional UI-locale override; defaults to the session locale. type: string enum: - de - en description: Optional UI-locale override; defaults to the session locale. responses: "200": description: Compliance assessment envelope (fresh, cached, or preparing). content: application/json: schema: $ref: "#/components/schemas/MedicationComplianceStatusResponseEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/insights/metric-status: get: tags: - Insights summary: Generic per-HealthKit-metric assessment description: "v1.8.7.1 — data-driven plain-language assessment for any registered HealthKit metric (resting heart rate, sleep, glucose, body composition, gait, audio exposure, …). One generic route covering ~30 metric pages via archetype prompt templates + per-metric metadata. Read-only: a cache miss warms a generation out of band and serves the last-good text meanwhile (stale-while-revalidate). An unknown `metric` 422s against the closed registry enum. Auth via cookie or Bearer." parameters: - in: query name: metric schema: type: string enum: - RESTING_HEART_RATE - HEART_RATE_VARIABILITY - OXYGEN_SATURATION - RESPIRATORY_RATE - BODY_TEMPERATURE - SKIN_TEMPERATURE - BLOOD_GLUCOSE - WALKING_HEART_RATE_AVERAGE - PULSE_WAVE_VELOCITY - VASCULAR_AGE - STEPS - ACTIVE_ENERGY - FLIGHTS_CLIMBED - WALKING_RUNNING_DISTANCE - TIME_IN_DAYLIGHT - VO2_MAX - TOTAL_BODY_WATER - BONE_MASS - FAT_FREE_MASS - FAT_MASS - MUSCLE_MASS - LEAN_BODY_MASS - VISCERAL_FAT - WALKING_STEADINESS - WALKING_ASYMMETRY - WALKING_DOUBLE_SUPPORT - WALKING_STEP_LENGTH - WALKING_SPEED - AUDIO_EXPOSURE_ENV - AUDIO_EXPOSURE_HEADPHONE - AUDIO_EXPOSURE_EVENT - SLEEP_DURATION - CARDIO_RECOVERY - WRIST_TEMPERATURE - FALL_COUNT - SIX_MINUTE_WALK_DISTANCE - STAIR_ASCENT_SPEED - STAIR_DESCENT_SPEED - BREATHING_DISTURBANCES description: "HealthKit metric id to assess (e.g. RESTING_HEART_RATE, SLEEP_DURATION). Closed enum: an unknown id 422s. The seven specialised metrics are served by their own routes and are not accepted here." required: true description: "HealthKit metric id to assess (e.g. RESTING_HEART_RATE, SLEEP_DURATION). Closed enum: an unknown id 422s. The seven specialised metrics are served by their own routes and are not accepted here." - in: query name: locale schema: description: Optional UI-locale override; defaults to the session locale. type: string enum: - de - en description: Optional UI-locale override; defaults to the session locale. responses: "200": description: Assessment envelope (fresh, cached, or preparing). content: application/json: schema: $ref: "#/components/schemas/MetricStatusResponseEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/insights/derived: get: tags: - Insights summary: Derived wellness metric (compute-once) description: v1.10.0 — the compute-once `Derived` value for any registered derived wellness metric (personal typical-range vitals baseline, cardio-fitness band, vascular-age delta, sleep score, readiness, coincident-deviation flag). One generic route over a closed registry enum; an unknown `metric` 422s. Pure compute over the rollup tier with a per-type live fallback on a coverage miss — no LLM call, no narrative, no cache table. Returns the flat `Derived` union so the native client can decode one stable shape and combine values across metrics. Auth via cookie or Bearer. parameters: - in: query name: metric schema: type: string enum: - VITALS_BASELINE - FITNESS_AGE - VASCULAR_AGE_DELTA - SLEEP_SCORE - READINESS - COINCIDENT_DEVIATION - HRV_BALANCE - BMI - RECOVERY_SCORE - STRESS_SCORE - STRAIN_SCORE - WRIST_TEMPERATURE_BASELINE - STAIR_ASCENT_SPEED_BASELINE - STAIR_DESCENT_SPEED_BASELINE - SIX_MINUTE_WALK_BAND - TRAJECTORY description: "Derived-metric id to compute (e.g. VITALS_BASELINE, FITNESS_AGE, VASCULAR_AGE_DELTA, HRV_BALANCE, BMI, READINESS). Closed enum: an unknown id 422s. Metrics whose compute has not yet landed return an `insufficient` value with reason `not_implemented`." required: true description: "Derived-metric id to compute (e.g. VITALS_BASELINE, FITNESS_AGE, VASCULAR_AGE_DELTA, HRV_BALANCE, BMI, READINESS). Closed enum: an unknown id 422s. Metrics whose compute has not yet landed return an `insufficient` value with reason `not_implemented`." - in: query name: type schema: description: For VITALS_BASELINE only — the single vital to band (defaults to RESTING_HEART_RATE). Ignored by composites. An unsupported value yields an `insufficient` value rather than a 422 so iOS metric combinations stay forgiving. type: string enum: - RESTING_HEART_RATE - HEART_RATE_VARIABILITY - RESPIRATORY_RATE - OXYGEN_SATURATION - BODY_TEMPERATURE - SKIN_TEMPERATURE - BLOOD_PRESSURE_SYS - BLOOD_PRESSURE_DIA - BLOOD_GLUCOSE - PULSE - WEIGHT description: For VITALS_BASELINE only — the single vital to band (defaults to RESTING_HEART_RATE). Ignored by composites. An unsupported value yields an `insufficient` value rather than a 422 so iOS metric combinations stay forgiving. responses: "200": description: The flat derived-metric value (ok or insufficient). content: application/json: schema: $ref: "#/components/schemas/DerivedMetricResponseEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/insights/derived/batch: get: tags: - Insights summary: Derived wellness metrics (batched compute-once) description: v1.10.0 — resolve several derived wellness metrics in ONE request. The `metrics` CSV names the metrics (a `metric:type` token sub-targets a VITALS_BASELINE vital); the server fans out under a bounded limiter with the profile loaded once and returns a map keyed by the per-request token. Collapses the Insights cold-mount fan-out of 14+ independent single-metric requests — the pool-starvation class that surfaces as a hang-then-recover. The single-metric route stays for the per-score detail pages. Auth via cookie or Bearer. parameters: - in: query name: metrics schema: type: string minLength: 1 maxLength: 1024 description: Comma-separated derived-metric tokens. Each is a `` or `:` (the colon sub-targets a VITALS_BASELINE vital). An unknown id 422s; a `type` outside the MeasurementType enum 422s; at most 24 tokens; duplicates collapse. required: true description: Comma-separated derived-metric tokens. Each is a `` or `:` (the colon sub-targets a VITALS_BASELINE vital). An unknown id 422s; a `type` outside the MeasurementType enum 422s; at most 24 tokens; duplicates collapse. responses: "200": description: The map of derived-metric values, keyed by token. content: application/json: schema: $ref: "#/components/schemas/DerivedBatchResponseEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/insights/correlations: get: tags: - Insights summary: Correlation discovery (FDR-controlled) description: v1.10.0 — scans a curated behaviour × outcome matrix (daylight / mood / glucose / BP / steps × sleep / HRV / resting HR / weight), lag-joins each behaviour day to the next day's outcome, runs Pearson with the exact Student-t p-value, and applies Benjamini-Hochberg FDR control across every tested pair. Only statistically-defensible pairs surface, each carrying n, r, p, and the BH-adjusted q. Descriptive, never causal. Gated by the operator `correlations` assistant surface. Auth via cookie or Bearer. responses: "200": description: The discovered correlations + the tested-pair count. content: application/json: schema: $ref: "#/components/schemas/CorrelationDiscoveryResponseEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/mood/tags: get: tags: - Mood summary: Effective per-user mood-tag tree (v1.8.5 / v1.13.0 / v1.17.0) description: "The fully-resolved, ordered Category → Tag tree: seeded catalogue + the caller's custom tags and groups, per-user layout (group order + placements) applied server-side, hidden catalogue tags omitted. `include` is a comma list: `hidden` (hidden catalogue tags, `hidden:true`), `archived` (own inactive custom tags, `archived:true`), `usage` (per-tag `usageCount`). With any flag set, the caller's own empty groups are kept; the plain read drops empty categories." parameters: - in: query name: include schema: description: Comma list of `hidden`, `archived`, `usage`. type: string description: Comma list of `hidden`, `archived`, `usage`. responses: "200": description: The effective tag tree. content: application/json: schema: $ref: "#/components/schemas/MoodTagTreeEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/mood/tags/custom: post: tags: - Mood summary: Create a custom mood tag (v1.13.0 / v1.17.0) description: "Mints a `custom:` key, encrypts the label at rest, stores a BINARY tag owned by the caller under the chosen group (default: the seeded `custom` category). 422 over the 50-tag cap or on an unknown / foreign `categoryKey`." requestBody: required: true content: application/json: schema: type: object properties: label: type: string minLength: 1 maxLength: 40 icon: anyOf: - type: string - type: "null" categoryKey: type: string minLength: 1 maxLength: 80 required: - label responses: "201": description: Custom tag created. content: application/json: schema: $ref: "#/components/schemas/MoodCustomTagCreatedEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/mood/tags/custom/{key}: patch: tags: - Mood summary: Update a custom mood tag (v1.13.0 / v1.17.0) description: "Owner-scoped: another user's key or a catalogue key 404s. Rename (re-encrypts), re-icon, archive/restore via `isActive`, or move groups via `categoryKey`." parameters: - in: path name: key schema: type: string required: true requestBody: required: true content: application/json: schema: type: object properties: label: type: string minLength: 1 maxLength: 40 icon: anyOf: - type: string - type: "null" isActive: type: boolean categoryKey: type: string minLength: 1 maxLength: 80 responses: "200": description: Custom tag updated. content: application/json: schema: $ref: "#/components/schemas/MoodCustomTagPatchEnvelope" "401": *a1 "404": &a5 description: Not found / not owned by the caller. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 delete: tags: - Mood summary: Archive or purge a custom mood tag (v1.13.0) description: "Default: soft-deactivates (`isActive:false`) — history intact, restorable via PATCH. `?purge=true`: hard-deletes the row; the FK cascade removes its entry links. Owner-scoped 404." parameters: - in: path name: key schema: type: string required: true - in: query name: purge schema: type: string enum: - "true" - "false" responses: "200": description: Archived (or purged). content: application/json: schema: $ref: "#/components/schemas/MoodCustomTagDeleteEnvelope" "401": *a1 "404": *a5 "422": *a3 "429": *a4 /api/mood/tags/{key}/hidden: put: tags: - Mood summary: Hide / show a catalogue mood tag (v1.13.0) description: Per-user hide of a CATALOGUE tag (upserts / removes a `mood_tag_hidden` row). 400 on a `custom:` key — custom tags archive via their own `isActive`. parameters: - in: path name: key schema: type: string required: true requestBody: required: true content: application/json: schema: type: object properties: hidden: type: boolean required: - hidden responses: "200": description: Visibility updated. content: application/json: schema: $ref: "#/components/schemas/MoodTagHiddenEnvelope" "400": description: Custom-tag key — use the custom PATCH instead. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "401": *a1 "404": *a5 "422": *a3 "429": *a4 /api/mood/tags/groups: post: tags: - Mood summary: Create a custom mood-tag group (v1.17.0) description: Mints a `customcat:` key, encrypts the label at rest, stores a group owned by the caller. 422 over the 12-group cap. requestBody: required: true content: application/json: schema: type: object properties: label: type: string minLength: 1 maxLength: 40 icon: anyOf: - type: string - type: "null" required: - label responses: "201": description: Custom group created. content: application/json: schema: $ref: "#/components/schemas/MoodTagGroupCreatedEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/mood/tags/groups/{key}: patch: tags: - Mood summary: Update a custom mood-tag group (v1.17.0) description: "Owner-scoped: another user's key or a seeded category key 404s. Rename (re-encrypts), re-icon, retire/restore via `isActive`." parameters: - in: path name: key schema: type: string required: true requestBody: required: true content: application/json: schema: type: object properties: label: type: string minLength: 1 maxLength: 40 icon: anyOf: - type: string - type: "null" isActive: type: boolean responses: "200": description: Custom group updated. content: application/json: schema: $ref: "#/components/schemas/MoodTagGroupPatchEnvelope" "401": *a1 "404": *a5 "422": *a3 "429": *a4 delete: tags: - Mood summary: Delete a custom mood-tag group (v1.17.0) description: "Non-destructive: the group's custom tags re-home to the seeded `custom` category, catalogue-tag placements evaporate back to their seeded category, the layout drops the group, then the row soft-deactivates (default) or hard-deletes (`?purge=true`). No tag and no entry link is deleted." parameters: - in: path name: key schema: type: string required: true - in: query name: purge schema: type: string enum: - "true" - "false" responses: "200": description: Group deleted; `rehomedCount` custom tags moved. content: application/json: schema: $ref: "#/components/schemas/MoodTagGroupDeleteEnvelope" "401": *a1 "404": *a5 "422": *a3 "429": *a4 /api/mood/tags/layout: get: tags: - Mood summary: Read the per-user mood-tag layout (v1.17.0) description: "Returns the stored blob merged over defaults: `groupOrder` resolved against the caller's effective category set (seeded + own groups), `placements` as stored." responses: "200": description: Resolved layout. content: application/json: schema: $ref: "#/components/schemas/MoodTagLayoutGetEnvelope" "401": *a1 "422": *a3 "429": *a4 put: tags: - Mood summary: Update the per-user mood-tag layout (v1.17.0) description: "Preserve-when-absent merge: a `groupOrder`-only PUT keeps the stored `placements` and vice versa. Bounded: ≤ 50 groups, ≤ 400 placement entries, keys ≤ 80 chars. Returns the resolved layout." requestBody: required: true content: application/json: schema: type: object properties: groupOrder: maxItems: 50 type: array items: type: string minLength: 1 maxLength: 80 placements: type: object propertyNames: type: string minLength: 1 maxLength: 80 additionalProperties: type: array items: type: string minLength: 1 maxLength: 80 responses: "200": description: Merged + resolved layout. content: application/json: schema: $ref: "#/components/schemas/MoodTagLayoutPutEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/settings/reminder-thresholds: get: tags: - Notifications summary: Read the medication reminder thresholds description: Returns the operator-level late/missed minute thresholds that tier an open dose's status, plus the calling user's low-stock runway threshold (days; null = alert off). One endpoint so every threshold consumer reads one shape. Auth via cookie or Bearer. responses: "200": description: Resolved thresholds. content: application/json: schema: $ref: "#/components/schemas/GetReminderThresholdsResponse" "401": *a1 "422": *a3 "429": *a4 /api/consent/ai: post: tags: - Consent summary: Grant AI consent (signed artefact) description: Persist a fresh AI-consent receipt. Required before server-managed AI egress (the operator's global LLM key); BYOK / local / ChatGPT-OAuth chains are the user's own egress and are not gated. Append-only audit trail. requestBody: required: true content: application/json: schema: type: object properties: kind: type: string enum: - ai_full - ai_insights_only - ai_coach artefact: type: string minLength: 1 signedAt: 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: - kind - artefact - signedAt responses: "200": description: The minted receipt. content: application/json: schema: $ref: "#/components/schemas/ConsentReceiptResponseEnvelope" "400": description: Request validation failed (consent routes use 400). content: application/json: schema: $ref: "#/components/schemas/ConsentValidationErrorEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/consent/ai/web: post: tags: - Consent summary: Grant web AI consent (idempotent ai_full) description: Mint an `ai_full` consent receipt for the calling web user if none is active; a no-op when one already exists. Mirrors the iOS master grant; the web AI-settings surface calls this on mount so existing web accounts on a shared-key deployment gain a receipt without a re-consent step. Revocation flows through DELETE /api/consent/ai/latest. responses: "200": description: Grant outcome. content: application/json: schema: $ref: "#/components/schemas/WebConsentGrantResponseEnvelope" "401": *a1 "422": *a3 "429": *a4 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 categoryValue: description: v1.10.0 — `HKCategoryValue` codepoint for an EVENT-class category sample (irregular-rhythm, high/low-HR, walking-steadiness, breathing-disturbance). Carries the device's own classification verdict / severity, which the server resolves to a stored `rhythmClassification`. HealthLog stores ONLY the device's result — it never re-classifies. Ignored for non-event identifiers. 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 source: description: Source attribution for the row. Defaults to `APPLE_HEALTH` when omitted. Send `MANUAL` to tag rows the user entered by hand on-device (e.g. the standalone adopt-on-pair backfill). Part of the `(userId, type, source, externalId)` dedup key, so the same externalId under a different source is a distinct row. `WITHINGS` / `IMPORT` are not accepted here. type: string enum: - APPLE_HEALTH - MANUAL 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). MeasurementsBulkDeleteRequest: type: object properties: ids: minItems: 1 maxItems: 200 type: array items: type: string minLength: 1 required: - ids description: 1..200 measurement ids, scoped to the caller. Forged / foreign ids are silently skipped (no existence leak). MeasurementsRestoreRequest: type: object properties: ids: minItems: 1 maxItems: 200 type: array items: type: string minLength: 1 required: - ids description: 1..200 measurement ids, scoped to the caller. Forged / foreign / not-deleted ids are silently skipped (no existence leak). DeviceRegisterRequest: type: object properties: token: type: string minLength: 8 maxLength: 512 pattern: ^[A-Za-z0-9+/=._:-]+$ description: Generic device identifier (legacy; APNs token below). bundleId: type: string minLength: 1 maxLength: 128 locale: type: string minLength: 2 maxLength: 16 appVersion: type: string minLength: 1 maxLength: 32 model: type: string minLength: 1 maxLength: 64 apnsToken: description: Hex-encoded APNs device token. Must be paired with `apnsEnvironment`. type: string minLength: 8 maxLength: 256 pattern: ^[A-Fa-f0-9]+$ apnsEnvironment: description: Gateway the iOS client received `apnsToken` from. Server never auto-detects. type: string enum: - sandbox - production medicationDelivery: description: v1.7.0 per-device medication-delivery override. NULL / omitted = inherit the user-level roaming default. "server" forces server APNs for this device; "client" forces local. Stored + echoed; cron suppression stays user-level. anyOf: - type: string enum: - server - client - type: "null" required: - token - bundleId description: Native device registration. Re-registering an APNs token belonging to another user returns 409 (cross-user-hijack guard). CreateMedicationRequest: type: object properties: name: type: string minLength: 1 maxLength: 100 dose: type: string minLength: 1 maxLength: 50 category: type: string enum: - BLOOD_PRESSURE - VITAMIN - SUPPLEMENT - PAIN_RELIEF - ALLERGY - DIGESTIVE - THYROID - HORMONE - SKIN - SLEEP_AID - DIABETES - ANTIBIOTIC - OTHER treatmentClass: type: string enum: - GENERIC - GLP1 dosesPerUnit: type: integer minimum: 1 maximum: 1000 unitsPerDose: description: Inventory units consumed per dose. A whole number 1–100 (e.g. 2 tablets of 2 mg for a 4 mg dose) or a supported fraction for a split pill (¼ / ⅓ / ½ / ⅔ / ¾). Default 1. The intake consumption hook decrements this many units per taken dose; dose-derived readouts divide unit counts by it. type: number deliveryForm: type: string enum: - ORAL - INJECTION - OTHER trackInjectionSites: description: Per-medication injection-site tracking opt-in. Default false. Only meaningful for an INJECTION delivery form; when true the client prompts (skippably) for the site after a taken dose. type: boolean allowedInjectionSites: description: Per-medication allowed / preferred injection sites. Empty array = no per-medication restriction (every site offered). The effective pickable set is this list minus the user's global exclusion. maxItems: 8 type: array items: type: string enum: - ABDOMEN_LEFT - ABDOMEN_RIGHT - ABDOMEN_UPPER_LEFT - ABDOMEN_UPPER_RIGHT - THIGH_LEFT - THIGH_RIGHT - UPPER_ARM_LEFT - UPPER_ARM_RIGHT notificationsEnabled: type: boolean liveActivityEnabled: description: iOS Live Activity opt-in for this medication's reminders. Default false. The iOS client owns the ActivityKit lifecycle; the server only stores + echoes the flag. type: boolean criticalAlarmEnabled: description: iOS 26 AlarmKit critical-reminder opt-in. Default false. Critical alarms bypass the device mute switch / Focus; the server stores the preference only and hangs no server-side behaviour off it. type: boolean atcCode: description: Optional WHO ATC classification code (active-substance class, 7 chars, e.g. `A10BX10`). User/clinician-asserted; never machine-guessed. Emitted on the FHIR `medicationCodeableConcept` under `http://www.whocc.no/atc`. NULL clears it; absent leaves it untouched. anyOf: - type: string pattern: ^[A-Z]\d{2}[A-Z]{2}\d{2}$ - type: "null" rxNormCode: description: Optional RxNorm RxCUI (numeric, US identifier, e.g. `2601723`). Secondary coding emitted under `http://www.nlm.nih.gov/research/umls/rxnorm` alongside any ATC code, never instead of the free-text name. NULL clears it; absent leaves it untouched. anyOf: - type: string maxLength: 20 pattern: ^\d+$ - type: "null" 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 asNeeded: description: "As-needed (PRN) medication (pain relief, rescue inhaler). When true the medication carries NO schedules (supplying any schedule entry is a 422): it is never due, never reminded, and excluded from compliance rates/streaks, but intakes still log (ad-hoc), inventory still consumes, and the history renders. Stays active indefinitely. Mutually exclusive with `oneShot`." type: boolean schedules: type: array items: $ref: "#/components/schemas/MedicationScheduleInput" required: - name - dose 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. v1.16.11 — `asNeeded: true` creates a PRN medication with ZERO schedules (`schedules` must be absent or empty, 422 otherwise); a scheduled medication still requires at least one schedule entry.' MedicationScheduleInput: type: object properties: windowStart: type: string pattern: ^([01]\d|2[0-3]):([0-5]\d)$ description: Legacy single-time-of-intake (HH:mm, user local). v1.5 keeps the field for backwards compatibility with pre-wizard iOS clients; the new `timesOfDay` array supersedes it. windowEnd: type: string pattern: ^([01]\d|2[0-3]):([0-5]\d)$ description: Legacy reminder-window upper bound (HH:mm). Used to derive the late-classification grace span when `reminderGraceMinutes` is null. label: description: Optional human label (e.g. "Morning", "Evening"). type: string maxLength: 50 dose: description: Per-schedule dose override. NULL means the schedule inherits `Medication.dose`. type: string maxLength: 50 daysOfWeek: description: Legacy day-of-week filter (0=Sunday..6=Saturday). v1.5 reads new writes through `rrule` first; this field is preserved for pre-v1.5 rows and is the input the route serialises into the persisted `days_of_week` string. type: array items: type: integer minimum: 0 maximum: 6 intervalWeeks: description: Legacy multi-week stride (1..4). Bi-weekly + tri-weekly were broken in the pre-v1.5 reminder worker; new writes encode the same intent via `rrule` (e.g. `FREQ=WEEKLY;INTERVAL=2;BYDAY=WE`). type: integer minimum: 1 maximum: 4 timesOfDay: description: v1.5 first-class points-in-time the dose is taken (HH:mm, user local). Up to 8 entries. Absent or empty means the route stamps `[windowStart]` so the new engine always sees a populated array. maxItems: 8 type: array items: type: string pattern: ^([01]\d|2[0-3]):([0-5]\d)$ reminderGraceMinutes: description: Reminder grace window in minutes. Caps at 24h. NULL falls back to the legacy `windowEnd - windowStart` span. type: integer minimum: 1 maximum: 1440 rrule: description: "RFC 5545 RRULE string (subset). Use for daily / weekly-with-BYDAY / multi-week / monthly / yearly cadences. Mutually exclusive with `rollingIntervalDays`. Examples: `FREQ=DAILY`, `FREQ=WEEKLY;BYDAY=MO,WE,FR`, `FREQ=WEEKLY;INTERVAL=2;BYDAY=WE`, `FREQ=MONTHLY;BYMONTHDAY=1`, `FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=1`." type: string maxLength: 200 pattern: ^FREQ=(?:DAILY|WEEKLY|MONTHLY|YEARLY)(?:;(?:INTERVAL=\d{1,3}|BYDAY=(?:MO|TU|WE|TH|FR|SA|SU)(?:,(?:MO|TU|WE|TH|FR|SA|SU))*|BYMONTHDAY=-?\d{1,2}(?:,-?\d{1,2})*|BYMONTH=\d{1,2}(?:,\d{1,2})*|COUNT=\d{1,4}|UNTIL=\d{8}T\d{6}Z))*$ rollingIntervalDays: description: Flexible-rolling interval in days, counted forward from the latest `MedicationIntakeEvent.takenAt` (the dose re-anchors when logged). Mutually exclusive with `rrule`. Range 1..365. type: integer minimum: 1 maximum: 365 scheduleType: description: Schedule type. SCHEDULED (default) = rrule / rolling / legacy cadence. PRN = as-needed (never projected, reminded, or counted in compliance expected; still loggable via the intake route). CYCLIC = N weeks on / M weeks off, gating whichever inner cadence the rrule / legacy fields describe. type: string enum: - SCHEDULED - PRN - CYCLIC cyclicOnWeeks: description: Cyclic "on" weeks (1..52). Required when `scheduleType` is CYCLIC; ignored otherwise. type: integer minimum: 1 maximum: 52 cyclicOffWeeks: description: Cyclic "off" weeks (0..52). Required when `scheduleType` is CYCLIC; ignored otherwise. type: integer minimum: 0 maximum: 52 doseWindows: description: Per-dose on-time intake windows. Each `{ timeOfDay, start, end }` HH:mm triple sets the explicit on-time band for the matching dose time; a dose time with no entry keeps the symmetric ±1h default. `timeOfDay` must match one of `timesOfDay` (or `windowStart`); `start <= end`. Up to 8 entries. Absent leaves every slot on the default derivation. The late tail stays cadence-derived. maxItems: 8 type: array items: $ref: "#/components/schemas/DoseWindowEntry" 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. DoseWindowEntry: type: object properties: timeOfDay: type: string pattern: ^([01]\d|2[0-3]):([0-5]\d)$ description: The dose time this window applies to (matches a `timesOfDay` entry). start: type: string pattern: ^([01]\d|2[0-3]):([0-5]\d)$ description: On-time band lower bound (HH:mm, user local). end: type: string pattern: ^([01]\d|2[0-3]):([0-5]\d)$ description: On-time band upper bound (HH:mm, user local). Must be >= `start`. required: - timeOfDay - start - end description: One explicit per-dose on-time intake window. `timeOfDay` matches a schedule dose time; `[start, end]` (HH:mm, user local, `start <= end`) is the on-time band. Outside it the cadence-derived late tail applies, then ad-hoc. MedicationListLayout: type: object properties: version: type: number const: 1 view: description: Which presentation /medications renders in. Default "cards". Optional on PUT — when omitted the stored value is preserved (preserve-when-absent, like `heroVisible` on the dashboard layout). Always present on responses. type: string enum: - cards - table order: description: User-defined manual medication order (medication ids, first = top), shared by both views. Display-only — unknown / deleted ids are ignored at render time, never 422. Optional on PUT (preserve-when-absent); always present on responses. maxItems: 200 type: array items: type: string minLength: 1 maxLength: 64 required: - version description: "Per-user /medications presentation: the card/table view choice plus the manual medication order shared by both views. Mirrors the dashboard-widgets / insights-layout contract." UpdateMedicationRequest: type: object properties: name: type: string minLength: 1 maxLength: 100 dose: type: string minLength: 1 maxLength: 50 category: type: string enum: - BLOOD_PRESSURE - VITAMIN - SUPPLEMENT - PAIN_RELIEF - ALLERGY - DIGESTIVE - THYROID - HORMONE - SKIN - SLEEP_AID - DIABETES - ANTIBIOTIC - OTHER treatmentClass: type: string enum: - GENERIC - GLP1 dosesPerUnit: anyOf: - type: integer minimum: 1 maximum: 1000 - type: "null" unitsPerDose: description: Inventory units consumed per dose — a whole number 1–100 or a supported split-pill fraction (¼ / ⅓ / ½ / ⅔ / ¾). The intake consumption hook decrements this many units per taken dose; already-stamped intake events keep their recorded consumption. type: number deliveryForm: type: string enum: - ORAL - INJECTION - OTHER trackInjectionSites: description: Per-medication injection-site tracking opt-in. Only meaningful for an INJECTION delivery form. Set false to deactivate tracking. type: boolean allowedInjectionSites: description: Per-medication allowed / preferred injection sites. Empty array clears the restriction. The effective pickable set subtracts the user's global exclusion (deny wins). maxItems: 8 type: array items: type: string enum: - ABDOMEN_LEFT - ABDOMEN_RIGHT - ABDOMEN_UPPER_LEFT - ABDOMEN_UPPER_RIGHT - THIGH_LEFT - THIGH_RIGHT - UPPER_ARM_LEFT - UPPER_ARM_RIGHT active: type: boolean notificationsEnabled: type: boolean liveActivityEnabled: description: iOS Live Activity opt-in for this medication's reminders. The iOS client owns the ActivityKit lifecycle; the server only stores + echoes the flag. type: boolean criticalAlarmEnabled: description: iOS 26 AlarmKit critical-reminder opt-in. Critical alarms bypass the device mute switch / Focus; the server stores the preference only. type: boolean atcCode: description: Optional WHO ATC classification code (active-substance class, 7 chars, e.g. `A10BX10`). User/clinician-asserted; never machine-guessed. Emitted on the FHIR `medicationCodeableConcept` under `http://www.whocc.no/atc`. NULL clears it; absent leaves it untouched. anyOf: - type: string pattern: ^[A-Z]\d{2}[A-Z]{2}\d{2}$ - type: "null" rxNormCode: description: Optional RxNorm RxCUI (numeric, US identifier, e.g. `2601723`). Secondary coding emitted under `http://www.nlm.nih.gov/research/umls/rxnorm` alongside any ATC code, never instead of the free-text name. NULL clears it; absent leaves it untouched. anyOf: - type: string maxLength: 20 pattern: ^\d+$ - type: "null" 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 asNeeded: description: "As-needed (PRN) medication (pain relief, rescue inhaler). When true the medication carries NO schedules (supplying any schedule entry is a 422): it is never due, never reminded, and excluded from compliance rates/streaks, but intakes still log (ad-hoc), inventory still consumes, and the history renders. Stays active indefinitely. Mutually exclusive with `oneShot`." type: boolean schedules: type: array items: $ref: "#/components/schemas/MedicationScheduleInput" reminderGraceMinutes: description: "Detail-page bridge: primary-schedule reminder-window in minutes. The route maps the value onto the primary schedule's `reminderGraceMinutes` field; ignored when a full `schedules` array is also supplied." anyOf: - type: integer minimum: 0 maximum: 1440 - type: "null" description: "Update-medication body. Every field is optional; omitted fields are left untouched. Supplying `schedules` REPLACES the medication's full schedule list (the route deletes existing rows before re-creating). Flipping `active` to false records the current timestamp on `pausedAt`; flipping back to true clears it. v1.5 invariants on the `schedules` array match the create path. v1.16.11 — `asNeeded: true` requires the medication to end schedule-less: supply `schedules: []` to clear an existing plan (any schedule entry is a 422); an empty `schedules` array without `asNeeded: true` is likewise a 422." 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. Must not lie in the future (5-minute clock-skew allowance) nor more than 5 years in the past. 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 injectionSite: description: Optional injection site for a taken dose. Honoured only when the medication is an INJECTION with site-tracking enabled; validated against the medication's effective allowed set (per-medication allowed sites minus the user's global exclusion). A disallowed site returns 422. Omit to record the dose without a site. type: string enum: - ABDOMEN_LEFT - ABDOMEN_RIGHT - ABDOMEN_UPPER_LEFT - ABDOMEN_UPPER_RIGHT - THIGH_LEFT - THIGH_RIGHT - UPPER_ARM_LEFT - UPPER_ARM_RIGHT forceSlotInstant: description: "Late-take override: pin this taken dose onto the named scheduled slot instead of orphaning it to an ad-hoc row. Must be a real scheduled slot of this medication on its day (validated server-side against the dose-window band anchors); an instant that is not a slot returns 422. Absent applies the default window-band attribution." 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)))$ doseTaken: description: Dose actually consumed for THIS intake when it deviates from (or documents) the medication's configured dose, e.g. a half tablet or a titration step. Free text, max 50 characters. Omit to record the take under the medication's configured dose. type: string minLength: 1 maxLength: 50 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. The optional `injectionSite` is persisted only for an INJECTION medication with site-tracking enabled and is validated against the medication's effective allowed set (422 on a disallowed value). CreateMedicationInventoryItemRequest: type: object properties: unitsTotal: type: number minimum: 1 maximum: 1000 description: Units the container ships with (tablets / ampoules / puffs; 1–1000, fractional allowed for split-pill packs). Dose-derived readouts divide by the medication's `unitsPerDose`. containerType: description: Kind of physical container (PEN / AMPOULE / BLISTER / INHALER / BOTTLE / OTHER). Display-level only; defaults to OTHER. type: string enum: - PEN - AMPOULE - BLISTER - INHALER - BOTTLE - OTHER printedExpiry: 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" purchasedAt: 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" notes: anyOf: - type: string maxLength: 200 - type: "null" required: - unitsTotal description: Register a new supply container (pen / blister pack / bottle). `unitsTotal` counts UNITS (1–1000); the item starts ACTIVE with `unitsRemaining = unitsTotal` and the intake consumption hook decrements it per taken dose. UpdateMedicationInventoryItemRequest: type: object properties: markAsFirstUseAt: description: Manually start the 30-day in-use clock (the user opened the container without logging an intake). ACTIVE flips to IN_USE; a backdated instant whose window already lapsed lands EXPIRED. 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)))$ markAsUsedUp: description: "Terminal override: zero the remaining units and mark the container USED_UP (physically discarded)." type: boolean printedExpiry: 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" unitsRemaining: description: Absolute remaining-unit correction (0–1000, fractional allowed). Clamped server-side to the item's `unitsTotal`; the canonical state machine re-derives the state (0 ⇒ USED_UP). type: number minimum: 0 maximum: 1000 notes: anyOf: - type: string maxLength: 200 - type: "null" description: "Per-item inventory mutation: manual first-use, used-up override, printed-expiry correction, absolute remaining-unit correction, notes. Every field is optional and commutative." MedicationExtractRequest: type: object properties: text: type: string minLength: 1 maxLength: 2000 description: Free-text medication description (any locale). Up to 2 000 characters. The model never echoes the text back into another tenant — it is only used to produce the structured fields. locale: description: Optional UI locale hint for the model. type: string enum: - en - de - es - fr - it - pl today: description: "Optional override of the reference date used to resolve relative phrases (\"tomorrow\", \"next Monday\"). Format: `YYYY-MM-DD`. Defaults to the server's UTC day." type: string pattern: ^\d{4}-\d{2}-\d{2}$ required: - text description: Free-text medication description payload. The route runs the text through the Coach provider chain and returns a partial structured payload the wizard merges. Rate-limited 10 requests / 5 minutes / user; budget-gated against the daily Coach token ceiling. CreateScheduleRevisionRequest: type: object properties: validFrom: 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)))$ description: Inclusive start instant of the manual era (ISO 8601). validUntil: 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)))$ description: Exclusive end instant of the manual era (ISO 8601). Must precede the current plan. timesOfDay: description: Daily dose times (HH:mm, user local) the era ran at. 1–8 entries; duplicates are collapsed. minItems: 1 maxItems: 8 type: array items: type: string pattern: ^([01]\d|2[0-3]):([0-5]\d)$ required: - validFrom - validUntil - timesOfDay description: "Manual schedule-era payload: era bounds plus the daily dose times that were live during the era." UpdateScheduleRevisionRequest: description: "Corrected schedule-era payload: replacement era bounds plus the daily dose times that were live during the era." $ref: "#/components/schemas/CreateScheduleRevisionRequest" 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. ProfileUpdateRequest: type: object properties: email: anyOf: - type: string format: email pattern: ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$ - type: "null" heightCm: anyOf: - type: number minimum: 50 maximum: 300 - type: "null" dateOfBirth: anyOf: - type: string - type: "null" gender: anyOf: - type: string enum: - MALE - FEMALE - type: "null" displayName: anyOf: - type: string minLength: 1 maxLength: 80 - type: "null" locale: anyOf: - type: string enum: - de - en - type: "null" timezone: type: string minLength: 1 maxLength: 64 timeFormat: description: Hour-cycle display preference. AUTO follows the locale convention, H12 forces AM/PM, H24 forces 24-hour. type: string enum: - AUTO - H12 - H24 moodReminderEnabled: type: boolean fullName: anyOf: - type: string maxLength: 120 - type: "null" insurerName: anyOf: - type: string maxLength: 120 - type: "null" insuranceNumber: description: German KVNR. Empty/null clears it; mod-10 check enforced. anyOf: - type: string - type: "null" insurerIkNumber: description: German insurer institution number (IKNR). Optional; empty/null clears it. A non-empty value must be exactly 9 digits (no checksum enforced). anyOf: - type: string - type: "null" description: Partial profile update. Every field is optional; an omitted field is left untouched, an explicit null (or empty string) clears it. `userId` is never accepted — it is narrowed from the session/token. NotificationPrefsPatchRequest: type: object properties: medication: type: object properties: clientManaged: type: boolean deliveryDefault: type: string enum: - server - client lowStockRunwayDays: anyOf: - type: integer minimum: 1 maximum: 60 - type: "null" mood: type: object properties: reminderHour: type: integer minimum: 0 maximum: 23 cycle: type: object properties: clientManaged: type: boolean coach: type: object properties: nudgesEnabled: type: boolean nudgeMedication: type: boolean nudgeVitals: type: boolean nudgeRoutine: type: boolean nudgeFrequency: type: string enum: - weekly - biweekly description: Partial per-user notification preferences. Every category and field is optional; the server deep-merges the supplied keys over the persisted row, so a PATCH touching only `medication` leaves the siblings intact. `medication.lowStockRunwayDays` (1–60, nullable) is the low-stock alert threshold in remaining runway days — `null` switches the alert off. InsightsLayoutBody: type: object properties: version: anyOf: - type: number const: 1 - type: number const: 2 sections: maxItems: 50 type: array items: type: object properties: id: type: string enum: - wellness-scores - daily-briefing - vitals - trends - period-review - cycle-summary - signals - rhythm-events visible: type: boolean order: type: integer minimum: 0 maximum: 99 required: - id - visible - order tiles: minItems: 1 maxItems: 50 type: array items: type: object properties: id: type: string enum: - overview - blood-pressure - pulse - oxygen - body-temperature - respiratory-rate - weight - bmi - body-water - bone-mass - fat-free-mass - fat-mass - muscle-mass - visceral-fat - lean-body-mass - steps - active-energy - workouts - flights-climbed - walking-distance - walking-steadiness - walking-heart-rate - walking-asymmetry - double-support-time - step-length - walking-speed - cardio-fitness - falls - six-minute-walk - stair-ascent-speed - stair-descent-speed - sleep - breathing-disturbances - resting-pulse - hrv - pulse-wave-velocity - vascular-age - cardio-recovery - environmental-audio - headphone-audio - audio-events - daylight - blood-glucose - skin-temperature - wrist-temperature - mood - medications - blutdruck - puls - sauerstoff - koerpertemperatur - gewicht - aktive-energie - schlaf - ruhepuls - stimmung - medikamente visible: type: boolean order: type: integer minimum: 0 maximum: 99 required: - id - visible - order required: - version description: "Per-user Insights layout (v2). `tiles` is the per-metric pill list (ordered, with a visibility flag); `sections` is the additive v2 list of the overview's big semantic blocks (wellness-scores, daily-briefing, vitals, trends, period-review, cycle-summary, signals, rhythm-events), each with order + visibility. `version` is the layout schema version: BOTH 1 and 2 are accepted on input (a pre-v2 iOS client still PUTs `version: 1`); the server always normalises to the canonical v2 blob before persisting, and GET responses always carry `version: 2`. Both arrays are optional on input — a client sending only `tiles` (the pre-v2 contract) still validates, and the server fills missing defaults; a v1 blob with no `sections` resolves forward with all sections default-visible. Tile ids are a closed enum: the canonical ids are English (matching the routed `/insights/` sub-pages). The legacy German tile ids (blutdruck, puls, sauerstoff, koerpertemperatur, gewicht, aktive-energie, schlaf, ruhepuls, stimmung, medikamente) remain accepted on input for backward compatibility and are normalised to their English equivalents before persisting; GET responses always carry the canonical English ids. Section ids are English-only. The legacy tile ids are deprecated and will be removed in a future major version." 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. InsightsPregenerateRequest: type: object properties: {} description: No body fields. The user is taken from the session / Bearer and the locale from the session; the warm covers every assessment for that user. MedicationTreatmentClass: type: string enum: - GENERIC - GLP1 description: Prisma-level treatment-class discriminator. `GLP1` unlocks the GLP-1 specialist surfaces (injection-site rotation, titration history, pen inventory, GLP-1-aware Coach). MedicationSchedule: type: object properties: id: type: string medicationId: type: string windowStart: type: string description: Legacy single-time-of-intake (HH:mm, user local). Preserved for backwards compatibility; the new `timesOfDay` array supersedes it. windowEnd: type: string description: Legacy reminder-window upper bound (HH:mm). Used to derive the late-classification grace span when `reminderGraceMinutes` is null. label: anyOf: - type: string - type: "null" dose: anyOf: - type: string - type: "null" description: Per-schedule dose override. NULL means the schedule inherits `Medication.dose`. daysOfWeek: anyOf: - type: string - type: "null" description: Legacy persisted recurrence encoding (`null` | `1,3,5` | `i2;1,3,5`). v1.5 readers consult `rrule` first; the field is kept for pre-v1.5 rows. v1.6.0 drops the column. timesOfDay: type: array items: type: string description: v1.5 first-class points-in-time the dose is taken (HH:mm, user local). Backfilled to `[windowStart]` for every pre-v1.5 row. reminderGraceMinutes: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: Reminder grace window in minutes. NULL falls back to the legacy `windowEnd - windowStart` span. rrule: anyOf: - type: string - type: "null" description: RFC 5545 RRULE string (subset). Used for calendar-anchored cadences. **Mutually exclusive with `rollingIntervalDays`** — exactly one of the two is non-null on any v1.5+ schedule (or both are null on legacy rows that haven't been touched since the migration). rollingIntervalDays: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: Flexible-rolling interval in days, counted forward from the latest `MedicationIntakeEvent.takenAt`. **Mutually exclusive with `rrule`.** scheduleType: type: string enum: - SCHEDULED - PRN - CYCLIC description: v1.7.0 schedule-type discriminator. SCHEDULED = rrule / rolling / legacy cadence. PRN = as-needed (never projected, reminded, or counted in compliance expected; still loggable via the intake route). CYCLIC = N weeks on / M weeks off, gating whichever inner cadence the rrule / legacy fields describe. cyclicOnWeeks: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: v1.7.0 cyclic "on" weeks. Only meaningful when `scheduleType` is CYCLIC; null otherwise. cyclicOffWeeks: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: v1.7.0 cyclic "off" weeks. Only meaningful when `scheduleType` is CYCLIC; null otherwise. required: - id - medicationId - windowStart - windowEnd - label - dose - daysOfWeek - timesOfDay - reminderGraceMinutes - rrule - rollingIntervalDays - scheduleType - cyclicOnWeeks - cyclicOffWeeks description: Schedule entry attached to a medication. v1.5 promotes `timesOfDay` to first-class and introduces `rrule` (calendar-anchored cadences) and `rollingIntervalDays` (flexible-rolling cadences). The two recurrence primitives are mutually exclusive — enforced by the Zod refine on writes, the route layer, and a DB CHECK constraint (`medication_schedules_rrule_xor_rolling`). v1.7.0 adds `scheduleType` (SCHEDULED / PRN / CYCLIC) and the cyclic on/off-week fields. CycleDayLogReadEnvelope: type: object properties: data: anyOf: - $ref: "#/components/schemas/CycleDayLogDTO" - type: "null" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CycleDayLogDTO: type: object properties: id: type: string date: type: string cycleId: anyOf: - type: string - type: "null" flow: anyOf: - type: string enum: - NONE - SPOTTING - LIGHT - MEDIUM - HEAVY - type: "null" intermenstrualBleeding: type: boolean basalBodyTempC: anyOf: - type: number - type: "null" temperatureExcluded: type: boolean ovulationTest: anyOf: - type: string enum: - NEGATIVE - POSITIVE_LH_SURGE - ESTROGEN_SURGE - INDETERMINATE - type: "null" cervicalMucus: anyOf: - type: string enum: - DRY - STICKY - CREAMY - WATERY - EGG_WHITE - type: "null" cervixPosition: anyOf: - type: string enum: - LOW - HIGH - type: "null" cervixFirmness: anyOf: - type: string enum: - FIRM - SOFT - type: "null" cervixOpening: anyOf: - type: string enum: - CLOSED - OPEN - type: "null" sexualActivity: type: boolean protectedSex: anyOf: - type: boolean - type: "null" pregnancyTest: anyOf: - type: string enum: - NEGATIVE - POSITIVE - INDETERMINATE - type: "null" progesteroneTest: anyOf: - type: string enum: - NEGATIVE - POSITIVE - INDETERMINATE - type: "null" contraceptive: anyOf: - type: string - type: "null" symptoms: type: array items: type: object properties: key: type: string severity: anyOf: - type: integer minimum: 1 maximum: 4 - type: "null" required: - key - severity additionalProperties: false note: anyOf: - type: string - type: "null" source: type: string externalId: anyOf: - type: string - type: "null" syncVersion: type: integer minimum: -9007199254740991 maximum: 9007199254740991 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)))$ deletedAt: 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" required: - id - date - cycleId - flow - intermenstrualBleeding - basalBodyTempC - temperatureExcluded - ovulationTest - cervicalMucus - cervixPosition - cervixFirmness - cervixOpening - sexualActivity - protectedSex - pregnancyTest - progesteroneTest - contraceptive - symptoms - note - source - externalId - syncVersion - updatedAt - deletedAt additionalProperties: false description: The canonical day-log row iOS mirrors. `note` is decrypted on read. Soft-deleted rows ride `/api/sync/changes` as tombstones. 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." CycleDayLogEnvelope: type: object properties: data: $ref: "#/components/schemas/CycleDayLogDTO" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CycleDayLogCreatedEnvelope: type: object properties: data: $ref: "#/components/schemas/CycleDayLogDTO" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CycleDayLogPatchEnvelope: type: object properties: data: $ref: "#/components/schemas/CycleDayLogDTO" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CycleBulkEnvelope: type: object properties: data: type: object properties: processed: type: integer minimum: -9007199254740991 maximum: 9007199254740991 inserted: type: integer minimum: -9007199254740991 maximum: 9007199254740991 updated: type: integer minimum: -9007199254740991 maximum: 9007199254740991 duplicates: type: integer minimum: -9007199254740991 maximum: 9007199254740991 skipped: type: integer minimum: -9007199254740991 maximum: 9007199254740991 entries: type: array items: type: object properties: index: type: integer minimum: -9007199254740991 maximum: 9007199254740991 status: type: string enum: - inserted - duplicate - updated - skipped id: type: string externalId: type: string reason: type: string required: - index - status additionalProperties: false required: - processed - inserted - updated - duplicates - skipped - entries additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CyclePeriodEnvelope: type: object properties: data: type: object properties: cycle: anyOf: - $ref: "#/components/schemas/MenstrualCycleDTO" - type: "null" dayLog: anyOf: - $ref: "#/components/schemas/CycleDayLogDTO" - type: "null" required: - cycle - dayLog additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MenstrualCycleDTO: type: object properties: id: type: string startDate: type: string endDate: anyOf: - type: string - type: "null" periodEndDate: anyOf: - type: string - type: "null" lengthDays: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" ovulationDate: anyOf: - type: string - type: "null" ovulationConfirmed: type: boolean isPredicted: type: boolean syncVersion: type: integer minimum: -9007199254740991 maximum: 9007199254740991 updatedAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - startDate - endDate - periodEndDate - lengthDays - ovulationDate - ovulationConfirmed - isPredicted - syncVersion - updatedAt additionalProperties: false description: One menstrual cycle (observed or forward-predicted). CycleCalendarEnvelope: type: object properties: data: type: object properties: profile: type: object properties: goal: type: string enum: - GENERAL_HEALTH - AVOID_PREGNANCY - TRYING_TO_CONCEIVE - PERIMENOPAUSE - OFF rawChartMode: type: boolean predictionEnabled: type: boolean cyclesObserved: type: integer minimum: -9007199254740991 maximum: 9007199254740991 required: - goal - rawChartMode - predictionEnabled - cyclesObserved additionalProperties: false prediction: anyOf: - $ref: "#/components/schemas/CyclePredictionDTO" - type: "null" stillLearning: type: boolean days: type: array items: type: object properties: date: type: string phase: anyOf: - $ref: "#/components/schemas/CyclePhase" - type: "null" isPredictedPeriod: type: boolean isFertileWindow: type: boolean isPredictedOvulation: type: boolean isPeriodLogged: type: boolean flow: anyOf: - type: string enum: - NONE - SPOTTING - LIGHT - MEDIUM - HEAVY - type: "null" hasSymptoms: type: boolean confidence: type: number basalBodyTempC: anyOf: - type: number - type: "null" temperatureExcluded: type: boolean ovulationTest: anyOf: - type: string enum: - NEGATIVE - POSITIVE_LH_SURGE - ESTROGEN_SURGE - INDETERMINATE - type: "null" cervicalMucus: anyOf: - type: string enum: - DRY - STICKY - CREAMY - WATERY - EGG_WHITE - type: "null" cervixPosition: anyOf: - type: string enum: - LOW - HIGH - type: "null" cervixFirmness: anyOf: - type: string enum: - FIRM - SOFT - type: "null" cervixOpening: anyOf: - type: string enum: - CLOSED - OPEN - type: "null" required: - date - phase - isPredictedPeriod - isFertileWindow - isPredictedOvulation - isPeriodLogged - flow - hasSymptoms - confidence - basalBodyTempC - temperatureExcluded - ovulationTest - cervicalMucus - cervixPosition - cervixFirmness - cervixOpening additionalProperties: false meta: type: object properties: generatedAt: 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: - generatedAt additionalProperties: false required: - profile - prediction - stillLearning - days - meta additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CyclePredictionDTO: type: object properties: method: $ref: "#/components/schemas/CyclePredictionMethod" nextPeriodStart: type: string nextPeriodStartLow: type: string nextPeriodStartHigh: type: string fertileWindowStart: anyOf: - type: string - type: "null" fertileWindowEnd: anyOf: - type: string - type: "null" predictedOvulation: anyOf: - type: string - type: "null" ovulationConfirmed: type: boolean confidence: type: number cyclesObserved: type: integer minimum: -9007199254740991 maximum: 9007199254740991 stillLearning: type: boolean disclaimer: type: string required: - method - nextPeriodStart - nextPeriodStartLow - nextPeriodStartHigh - fertileWindowStart - fertileWindowEnd - predictedOvulation - ovulationConfirmed - confidence - cyclesObserved - stillLearning - disclaimer additionalProperties: false description: The materialised forecast. Fertile-window fields (and predictedOvulation/ovulationConfirmed) are server-suppressed (null/false) unless the goal is TRYING_TO_CONCEIVE or AVOID_PREGNANCY. CyclePredictionMethod: type: string enum: - CALENDAR - SYMPTOTHERMAL - TEMPERATURE_TREND - BLENDED CyclePhase: type: string enum: - MENSTRUAL - FOLLICULAR - OVULATORY - LUTEAL CycleHistoryEnvelope: type: object properties: data: type: object properties: cycles: type: array items: $ref: "#/components/schemas/MenstrualCycleDTO" stats: type: object properties: avgLengthDays: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" lengthVariabilityDays: anyOf: - type: number - type: "null" avgPeriodLengthDays: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" regularity: type: string enum: - REGULAR - IRREGULAR - LEARNING required: - avgLengthDays - lengthVariabilityDays - avgPeriodLengthDays - regularity additionalProperties: false required: - cycles - stats additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CycleInsightsEnvelope: type: object properties: data: type: object properties: rows: type: array items: $ref: "#/components/schemas/CyclePhaseCrosstabRow" headline: anyOf: - $ref: "#/components/schemas/CyclePhaseCrosstabRow" - type: "null" lagged: type: object properties: discovered: type: array items: $ref: "#/components/schemas/CyclePhaseLaggedPair" pairsTested: type: integer minimum: -9007199254740991 maximum: 9007199254740991 fdrQ: type: number minPairs: type: integer minimum: -9007199254740991 maximum: 9007199254740991 required: - discovered - pairsTested - fdrQ - minPairs additionalProperties: false symptomPatterns: type: array items: $ref: "#/components/schemas/CycleSymptomPhaseRow" contrast: type: object properties: high: type: string const: LUTEAL low: type: string const: FOLLICULAR required: - high - low additionalProperties: false windowDays: type: integer minimum: -9007199254740991 maximum: 9007199254740991 cyclesObserved: type: integer minimum: -9007199254740991 maximum: 9007199254740991 required: - rows - headline - lagged - symptomPatterns - contrast - windowDays - cyclesObserved additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CyclePhaseCrosstabRow: type: object properties: metricKey: type: string enum: - restingHeartRate - heartRateVariability - sleepDuration - steps - weight - basalBodyTemp - wristTemperature - skinTemperature - bloodGlucose - mood display: type: string enum: - hours - steps - bpm - ms - kg - celsius - glucose - mood lutealDays: type: integer minimum: -9007199254740991 maximum: 9007199254740991 follicularDays: type: integer minimum: -9007199254740991 maximum: 9007199254740991 lutealAvg: type: number follicularAvg: type: number delta: type: number pValue: type: number qValue: type: number confidence: type: string enum: - low - medium - high required: - metricKey - display - lutealDays - follicularDays - lutealAvg - follicularAvg - delta - pValue - qValue - confidence additionalProperties: false description: One FDR-surviving luteal-vs-follicular contrast for an outcome metric. `delta = lutealAvg − follicularAvg`. Observational, never causal. CyclePhaseLaggedPair: type: object properties: behaviour: type: string outcome: type: string n: type: integer minimum: -9007199254740991 maximum: 9007199254740991 r: type: number pValue: type: number qValue: type: number interpretation: type: string lagDays: type: integer minimum: -9007199254740991 maximum: 9007199254740991 required: - behaviour - outcome - n - r - pValue - qValue - interpretation - lagDays additionalProperties: false description: One FDR-surviving lagged-Pearson pair from the continuous CYCLE_PHASE ordinal × outcome matrix (mechanism B). Descriptive, never causal. CycleSymptomPhaseRow: type: object properties: symptomKey: type: string counts: type: object properties: MENSTRUAL: type: integer minimum: -9007199254740991 maximum: 9007199254740991 FOLLICULAR: type: integer minimum: -9007199254740991 maximum: 9007199254740991 OVULATORY: type: integer minimum: -9007199254740991 maximum: 9007199254740991 LUTEAL: type: integer minimum: -9007199254740991 maximum: 9007199254740991 required: - MENSTRUAL - FOLLICULAR - OVULATORY - LUTEAL additionalProperties: false total: type: integer minimum: -9007199254740991 maximum: 9007199254740991 topPhase: type: string enum: - MENSTRUAL - FOLLICULAR - OVULATORY - LUTEAL topShare: type: number required: - symptomKey - counts - total - topPhase - topShare additionalProperties: false description: Where a logged symptom clusters across the cycle phases. Surfaced only once logged on ≥3 phase-labelled days. Observational, never causal. CyclePurgeEnvelope: type: object properties: data: type: object properties: purged: type: boolean dayLogs: type: integer minimum: -9007199254740991 maximum: 9007199254740991 predictions: type: integer minimum: -9007199254740991 maximum: 9007199254740991 cycles: type: integer minimum: -9007199254740991 maximum: 9007199254740991 auditRows: type: integer minimum: -9007199254740991 maximum: 9007199254740991 pushRows: type: integer minimum: -9007199254740991 maximum: 9007199254740991 required: - purged - dayLogs - predictions - cycles - auditRows - pushRows additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CycleProfileEnvelope: type: object properties: data: $ref: "#/components/schemas/CycleProfileDTO" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CycleProfileDTO: type: object properties: goal: type: string enum: - GENERAL_HEALTH - AVOID_PREGNANCY - TRYING_TO_CONCEIVE - PERIMENOPAUSE - OFF cycleTrackingEnabled: type: boolean secondarySymptom: type: string enum: - MUCUS - CERVIX rawChartMode: type: boolean predictionEnabled: type: boolean discreetNotifications: type: boolean sensitiveCategoryEncryption: type: boolean typicalCycleLength: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" typicalPeriodLength: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" lutealPhaseLength: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" updatedAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - goal - cycleTrackingEnabled - secondarySymptom - rawChartMode - predictionEnabled - discreetNotifications - sensitiveCategoryEncryption - typicalCycleLength - typicalPeriodLength - lutealPhaseLength - updatedAt additionalProperties: false description: The full per-user cycle settings row. CyclePrefsGetEnvelope: type: object properties: data: $ref: "#/components/schemas/CycleProfileDTO" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CyclePrefsPatchEnvelope: type: object properties: data: $ref: "#/components/schemas/CycleProfileDTO" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CapabilitiesEnvelope: type: object properties: data: $ref: "#/components/schemas/CapabilitiesResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CapabilitiesResponse: type: object properties: apiContractVersion: type: string description: Running build version — mirrors GET /api/version `version`. derivedMetricIds: type: array items: type: string description: Closed derived-metric id set (GET /api/insights/derived). vitalsBaselineTypes: type: array items: type: string description: MeasurementTypes the typical-range baseline engine supports. layoutTileIds: type: array items: type: string description: Canonical insights layout tile-id set (English ids). metricStatusIds: type: array items: type: string description: Closed metric-status / assessment id set. ingest: type: object properties: quantityTypes: type: array items: type: object properties: type: type: string description: HealthLog MeasurementType. hk: type: string description: HealthKit identifier. unit: type: string description: Canonical DB unit. required: - type - hk - unit additionalProperties: false description: Accepted HealthKit quantity-sample mappings. eventTypes: type: array items: type: string description: MeasurementTypes for device-flagged EVENT-class HealthKit samples. computedScores: type: array items: type: string description: Server-owned nightly composite score types. writeAllowlist: type: array items: type: string description: MeasurementSources a client may attribute on a write (others are server-owned). required: - quantityTypes - eventTypes - computedScores - writeAllowlist additionalProperties: false description: Ingest vocabularies the batch / single-write paths accept. fhir: type: object properties: atcSystem: type: string description: WHO ATC CodeSystem URI. snomedRoute: type: string description: SNOMED CT CodeSystem URI. germanAtcDefaultLocales: type: array items: type: string description: App locales that default the additive BfArM ATC coding on. restBaseUrl: type: string description: Base path of the read-only FHIR R4 REST face. readScope: type: string description: Bearer scope a narrow token needs to read the FHIR face. resourceTypes: type: array items: type: string description: FHIR resource types the REST face serves (read + search). operations: type: array items: type: string description: Whole-record operations exposed (e.g. $everything). searchParams: type: array items: type: string description: Search parameters honoured uniformly across the search routes. required: - atcSystem - snomedRoute - germanAtcDefaultLocales - restBaseUrl - readScope - resourceTypes - operations - searchParams additionalProperties: false description: FHIR coding constants + the read-only REST face descriptor (v1.11). share: type: object properties: supported: type: boolean description: Whether clinician share links are served. maxDays: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Maximum lifetime of a share link, in days. No never-expiring share. resourceTypes: type: array items: type: string description: FHIR resource types a share link may be scoped to serve. sections: type: array items: type: string description: Scopeable report sections a share link may toggle. required: - supported - maxDays - resourceTypes - sections additionalProperties: false description: Clinician share-link surface descriptor (v1.11). required: - apiContractVersion - derivedMetricIds - vitalsBaselineTypes - layoutTileIds - metricStatusIds - ingest - fhir - share additionalProperties: false description: Live id vocabularies + contract version. Every list is derived server-side from the canonical registry it documents, so it cannot drift from the values the routes actually accept/emit. ShareLinkCreated: type: object properties: data: type: object properties: id: type: string label: type: string rangeStart: type: string rangeEnd: anyOf: - type: string - type: "null" resourceTypes: type: array items: type: string allowFhirApi: type: boolean expiresAt: type: string createdAt: type: string revokedAt: anyOf: - type: string - type: "null" lastAccessAt: anyOf: - type: string - type: "null" accessCount: type: number active: type: boolean token: type: string description: Raw `hls_` token — returned ONCE and unrecoverable thereafter. required: - id - label - rangeStart - rangeEnd - resourceTypes - allowFhirApi - expiresAt - createdAt - revokedAt - lastAccessAt - accessCount - active - token additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false ShareLinkList: type: object properties: data: type: object properties: shareLinks: type: array items: type: object properties: id: type: string label: type: string rangeStart: type: string rangeEnd: anyOf: - type: string - type: "null" resourceTypes: type: array items: type: string allowFhirApi: type: boolean expiresAt: type: string createdAt: type: string revokedAt: anyOf: - type: string - type: "null" lastAccessAt: anyOf: - type: string - type: "null" accessCount: type: number active: type: boolean required: - id - label - rangeStart - rangeEnd - resourceTypes - allowFhirApi - expiresAt - createdAt - revokedAt - lastAccessAt - accessCount - active additionalProperties: false required: - shareLinks additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false ShareLinkRevoked: type: object properties: data: type: object properties: id: type: string revoked: type: boolean required: - id - revoked additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false LoginResponse: type: object properties: data: $ref: "#/components/schemas/AccessRefreshBundle" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false AccessRefreshBundle: type: object properties: user: type: object properties: id: type: string username: type: string required: - id - username additionalProperties: false token: type: string description: Access token (`hlk_<64hex>`). tokenExpiresAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ refreshToken: description: Refresh token (`hlr_<64hex>`); only present for native-policy callers. type: string refreshTokenExpiresAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - token - tokenExpiresAt additionalProperties: false description: Native-client token bundle returned by login + refresh. Web cookie-only callers see only `user`. PasskeyLoginVerifyResponse: type: object properties: data: $ref: "#/components/schemas/AccessRefreshBundle" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false RefreshResponse: type: object properties: data: $ref: "#/components/schemas/AccessRefreshBundle" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false SyncStateEnvelope: type: object properties: data: $ref: "#/components/schemas/SyncStateResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false SyncStateResponse: type: object properties: userId: type: string timezone: type: string lastSyncedAt: anyOf: - type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" serverNow: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ measurements: type: object properties: lastUpdatedAt: anyOf: - type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" liveCount: type: integer minimum: 0 maximum: 9007199254740991 tombstonedCount: type: integer minimum: 0 maximum: 9007199254740991 required: - lastUpdatedAt - liveCount - tombstonedCount additionalProperties: false mood: type: object properties: lastUpdatedAt: anyOf: - type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" liveCount: type: integer minimum: 0 maximum: 9007199254740991 tombstonedCount: type: integer minimum: 0 maximum: 9007199254740991 required: - lastUpdatedAt - liveCount - tombstonedCount additionalProperties: false intakes: type: object properties: lastUpdatedAt: anyOf: - type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" liveCount: type: integer minimum: 0 maximum: 9007199254740991 tombstonedCount: type: integer minimum: 0 maximum: 9007199254740991 required: - lastUpdatedAt - liveCount - tombstonedCount additionalProperties: false sync: type: object properties: incrementalWindowDays: type: integer exclusiveMinimum: 0 maximum: 9007199254740991 description: Days an incremental delta stays valid; tracks the native refresh-token lifetime. Beyond it a device re-pairs with a full backfill. iOS derives its window from this rather than hardcoding 60. tombstoneRetentionDays: type: integer exclusiveMinimum: 0 maximum: 9007199254740991 description: Horizon past which tombstones may be pruned. A cursor older than this gets `cursorExpired` on `/api/sync/changes`. required: - incrementalWindowDays - tombstoneRetentionDays additionalProperties: false description: Sync-window metadata the client reads instead of hardcoding. required: - userId - timezone - lastSyncedAt - serverNow - measurements - mood - intakes - sync additionalProperties: false description: iOS SyncMode handshake. Each GET also advances the server-side `lastSyncedAt` checkpoint and returns the previous value. The cheap 'should I sync?' summary; the durable delta cursor lives on `/api/sync/changes`. SyncChangesEnvelope: type: object properties: data: $ref: "#/components/schemas/SyncChangesResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false SyncChangesResponse: type: object properties: serverNow: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ cursor: anyOf: - type: string - type: "null" description: Opaque cursor to echo into the next request. hasMore: type: boolean description: False once the client is caught up as of `serverNow`. cursorExpired: type: boolean description: True when the supplied cursor predates tombstone retention — drop the cursor and do a clean initial sync. changes: type: object properties: measurements: type: object properties: upserts: type: array items: $ref: "#/components/schemas/SyncMeasurementUpsert" tombstones: type: array items: $ref: "#/components/schemas/SyncMeasurementTombstone" required: - upserts - tombstones additionalProperties: false mood: type: object properties: upserts: type: array items: $ref: "#/components/schemas/SyncMoodUpsert" tombstones: type: array items: $ref: "#/components/schemas/SyncMoodTombstone" required: - upserts - tombstones additionalProperties: false intakes: type: object properties: upserts: type: array items: $ref: "#/components/schemas/SyncIntakeUpsert" tombstones: type: array items: $ref: "#/components/schemas/SyncIntakeTombstone" required: - upserts - tombstones additionalProperties: false required: - measurements - mood - intakes additionalProperties: false required: - serverNow - cursor - hasMore - cursorExpired - changes additionalProperties: false description: "Multi-domain delta page (v1.7.0): measurements + mood + intakes. One opaque multi-domain keyset cursor; tombstones apply before upserts within each domain. Tombstone identity: measurements key on externalId, mood + intakes on server id. The iOS consumer is measurements-only this cycle; mood + intakes are forward-prep." SyncMeasurementUpsert: type: object properties: id: type: string type: type: string enum: - WEIGHT - BLOOD_PRESSURE_SYS - BLOOD_PRESSURE_DIA - PULSE - BODY_FAT - SLEEP_DURATION - ACTIVITY_STEPS - BLOOD_GLUCOSE - TOTAL_BODY_WATER - BONE_MASS - OXYGEN_SATURATION - HEART_RATE_VARIABILITY - RESTING_HEART_RATE - ACTIVE_ENERGY_BURNED - FLIGHTS_CLIMBED - WALKING_RUNNING_DISTANCE - VO2_MAX - BODY_TEMPERATURE - FAT_FREE_MASS - FAT_MASS - MUSCLE_MASS - SKIN_TEMPERATURE - PULSE_WAVE_VELOCITY - VASCULAR_AGE - VISCERAL_FAT - AUDIO_EXPOSURE_ENV - AUDIO_EXPOSURE_HEADPHONE - TIME_IN_DAYLIGHT - WALKING_STEADINESS - AUDIO_EXPOSURE_EVENT - RESPIRATORY_RATE - BODY_MASS_INDEX - LEAN_BODY_MASS - WALKING_HEART_RATE_AVERAGE - WALKING_ASYMMETRY - WALKING_DOUBLE_SUPPORT - WALKING_STEP_LENGTH - WALKING_SPEED - CARDIO_RECOVERY - WRIST_TEMPERATURE - FALL_COUNT - SIX_MINUTE_WALK_DISTANCE - STAIR_ASCENT_SPEED - STAIR_DESCENT_SPEED - BREATHING_DISTURBANCES - IRREGULAR_RHYTHM_NOTIFICATION - HIGH_HEART_RATE_EVENT - LOW_HEART_RATE_EVENT - WALKING_STEADINESS_EVENT - BREATHING_DISTURBANCE_EVENT - RECOVERY_SCORE - STRESS_SCORE - STRAIN_SCORE - HRV_RMSSD - DAY_STRAIN - WORKOUT_STRAIN - SLEEP_PERFORMANCE - SLEEP_EFFICIENCY - SLEEP_CONSISTENCY - SLEEP_NEED - ENERGY_EXPENDITURE_KJ - AVERAGE_HEART_RATE - MAX_HEART_RATE - SLEEP_DISTURBANCE_COUNT 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 - COMPUTED - WHOOP - FITBIT notes: anyOf: - type: string - type: "null" externalId: anyOf: - type: string - type: "null" description: Cross-device dedup key (UUID string or `stats::`). syncVersion: type: integer exclusiveMinimum: 0 maximum: 9007199254740991 description: LWW reconciliation counter; echo to keep the mirror monotonic. updatedAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - type - value - unit - measuredAt - source - externalId - syncVersion - updatedAt additionalProperties: false SyncMeasurementTombstone: type: object properties: id: type: string externalId: anyOf: - type: string - type: "null" description: The identity key the client dedups on for measurements. syncVersion: type: integer exclusiveMinimum: 0 maximum: 9007199254740991 deletedAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ updatedAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - externalId - syncVersion - deletedAt - updatedAt additionalProperties: false description: A soft-deleted measurement. Apply tombstones BEFORE upserts within a page to avoid resurrecting a row. SyncMoodUpsert: type: object properties: id: type: string date: type: string description: YYYY-MM-DD anchored to the row's `tz`. mood: type: string score: type: integer minimum: -9007199254740991 maximum: 9007199254740991 tags: anyOf: - type: string - type: "null" description: JSON array of tag keys, or null. note: anyOf: - type: string - type: "null" moodLoggedAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ source: type: string syncVersion: type: integer minimum: 0 maximum: 9007199254740991 description: LWW reconciliation counter; mood is last-writer-wins by it. updatedAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - date - mood - score - tags - note - moodLoggedAt - source - syncVersion - updatedAt additionalProperties: false SyncMoodTombstone: type: object properties: id: type: string description: Server id — the identity key the client dedups on for mood. syncVersion: type: integer minimum: 0 maximum: 9007199254740991 deletedAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ updatedAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - syncVersion - deletedAt - updatedAt additionalProperties: false description: A soft-deleted mood entry, keyed on server `id`. Apply before upserts within the domain page. SyncIntakeUpsert: type: object properties: id: type: string medicationId: type: string scheduledFor: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ takenAt: anyOf: - type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" skipped: type: boolean source: type: string syncVersion: type: integer minimum: 0 maximum: 9007199254740991 description: Reconciliation counter. An intake is immutable; a correction is a tombstone + re-insert. updatedAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - medicationId - scheduledFor - takenAt - skipped - source - syncVersion - updatedAt additionalProperties: false SyncIntakeTombstone: type: object properties: id: type: string description: Server id — the identity key the client dedups on for intakes. syncVersion: type: integer minimum: 0 maximum: 9007199254740991 deletedAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ updatedAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - syncVersion - deletedAt - updatedAt additionalProperties: false description: A soft-deleted medication intake, keyed on server `id`. Apply before upserts within the domain page. GetMeasurementsSeriesResponse: type: object properties: data: $ref: "#/components/schemas/MeasurementsSeriesResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MeasurementsSeriesResponse: type: object properties: kind: type: string enum: - weight - bloodPressure - pulse - bodyFat - glucose - sleep - steps - totalBodyWater - boneMass - oxygenSaturation - restingHeartRate - heartRateVariability - vo2Max unit: type: string description: Resolved unit token for `value`. `glucose` follows the user's mg/dL|mmol/L preference; `sleep` is `h` (per-night time-asleep in hours). points: type: array items: type: object properties: id: type: string description: Measurement row id, or `sleep:` for a sleep night. 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)))$ description: Point timestamp (ISO-8601). value: type: number description: Primary value in the top-level `unit`. secondary: anyOf: - type: number - type: "null" description: Diastolic value for `kind=bloodPressure` (paired within ±5 min); null for every other kind. sleepStages: description: Per-stage hours for a `kind=sleep` night (CORE/DEEP/REM/…); null/absent for non-sleep kinds. anyOf: - type: object propertyNames: type: string additionalProperties: type: number - type: "null" required: - id - at - value - secondary additionalProperties: false stats: type: object properties: mean: type: number min: type: number max: type: number stdDev: type: number count: type: integer minimum: 0 maximum: 9007199254740991 required: - mean - min - max - stdDev - count additionalProperties: false description: Summary over the returned points; all-zero when empty. required: - kind - unit - points - stats additionalProperties: false description: "iOS-friendly per-kind time series: one point per reading (or per reconstructed night for `sleep`), an explicit `unit` token, and a summary `stats` block." ListMeasurementsResponse: type: object properties: data: type: object properties: measurements: type: array items: $ref: "#/components/schemas/MeasurementResource" total: type: integer minimum: 0 maximum: 9007199254740991 required: - measurements - total additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MeasurementResource: type: object properties: id: type: string type: type: string enum: - WEIGHT - BLOOD_PRESSURE_SYS - BLOOD_PRESSURE_DIA - PULSE - BODY_FAT - SLEEP_DURATION - ACTIVITY_STEPS - BLOOD_GLUCOSE - TOTAL_BODY_WATER - BONE_MASS - OXYGEN_SATURATION - HEART_RATE_VARIABILITY - RESTING_HEART_RATE - ACTIVE_ENERGY_BURNED - FLIGHTS_CLIMBED - WALKING_RUNNING_DISTANCE - VO2_MAX - BODY_TEMPERATURE - FAT_FREE_MASS - FAT_MASS - MUSCLE_MASS - SKIN_TEMPERATURE - PULSE_WAVE_VELOCITY - VASCULAR_AGE - VISCERAL_FAT - AUDIO_EXPOSURE_ENV - AUDIO_EXPOSURE_HEADPHONE - TIME_IN_DAYLIGHT - WALKING_STEADINESS - AUDIO_EXPOSURE_EVENT - RESPIRATORY_RATE - BODY_MASS_INDEX - LEAN_BODY_MASS - WALKING_HEART_RATE_AVERAGE - WALKING_ASYMMETRY - WALKING_DOUBLE_SUPPORT - WALKING_STEP_LENGTH - WALKING_SPEED - CARDIO_RECOVERY - WRIST_TEMPERATURE - FALL_COUNT - SIX_MINUTE_WALK_DISTANCE - STAIR_ASCENT_SPEED - STAIR_DESCENT_SPEED - BREATHING_DISTURBANCES - IRREGULAR_RHYTHM_NOTIFICATION - HIGH_HEART_RATE_EVENT - LOW_HEART_RATE_EVENT - WALKING_STEADINESS_EVENT - BREATHING_DISTURBANCE_EVENT - RECOVERY_SCORE - STRESS_SCORE - STRAIN_SCORE - HRV_RMSSD - DAY_STRAIN - WORKOUT_STRAIN - SLEEP_PERFORMANCE - SLEEP_EFFICIENCY - SLEEP_CONSISTENCY - SLEEP_NEED - ENERGY_EXPENDITURE_KJ - AVERAGE_HEART_RATE - MAX_HEART_RATE - SLEEP_DISTURBANCE_COUNT 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 - COMPUTED - WHOOP - FITBIT 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 MeasurementsBulkDeleteResponse: type: object properties: data: $ref: "#/components/schemas/MeasurementsBulkDeleteResult" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MeasurementsBulkDeleteResult: type: object properties: deleted: type: integer minimum: 0 maximum: 9007199254740991 required: - deleted additionalProperties: false MeasurementsRestoreResponse: type: object properties: data: $ref: "#/components/schemas/MeasurementsRestoreResult" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MeasurementsRestoreResult: type: object properties: restored: type: integer minimum: 0 maximum: 9007199254740991 required: - restored additionalProperties: false SleepNightResponse: type: object properties: data: $ref: "#/components/schemas/SleepNightResource" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false SleepNightResource: type: object properties: night: anyOf: - type: string - type: "null" main: anyOf: - type: object properties: night: type: string description: Wake-day key (YYYY-MM-DD). source: anyOf: - type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - type: "null" start: 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)))$ end: 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)))$ asleepMinutes: type: integer minimum: 0 maximum: 9007199254740991 inBedMinutes: anyOf: - type: integer minimum: 0 maximum: 9007199254740991 - type: "null" awakeMinutes: anyOf: - type: integer minimum: 0 maximum: 9007199254740991 - type: "null" awakenings: type: integer minimum: 0 maximum: 9007199254740991 stages: type: object propertyNames: type: string enum: - IN_BED - AWAKE - ASLEEP - REM - CORE - DEEP additionalProperties: type: integer minimum: 0 maximum: 9007199254740991 required: - IN_BED - AWAKE - ASLEEP - REM - CORE - DEEP segments: type: array items: type: object properties: stage: anyOf: - type: string enum: - IN_BED - AWAKE - ASLEEP - REM - CORE - DEEP - type: "null" start: 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)))$ end: 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)))$ minutes: type: integer minimum: 0 maximum: 9007199254740991 required: - stage - start - end - minutes additionalProperties: false required: - night - source - start - end - asleepMinutes - inBedMinutes - awakeMinutes - awakenings - stages - segments additionalProperties: false - type: "null" naps: type: array items: type: object properties: night: type: string description: Wake-day key (YYYY-MM-DD). source: anyOf: - type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - type: "null" start: 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)))$ end: 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)))$ asleepMinutes: type: integer minimum: 0 maximum: 9007199254740991 inBedMinutes: anyOf: - type: integer minimum: 0 maximum: 9007199254740991 - type: "null" awakeMinutes: anyOf: - type: integer minimum: 0 maximum: 9007199254740991 - type: "null" awakenings: type: integer minimum: 0 maximum: 9007199254740991 stages: type: object propertyNames: type: string enum: - IN_BED - AWAKE - ASLEEP - REM - CORE - DEEP additionalProperties: type: integer minimum: 0 maximum: 9007199254740991 required: - IN_BED - AWAKE - ASLEEP - REM - CORE - DEEP segments: type: array items: type: object properties: stage: anyOf: - type: string enum: - IN_BED - AWAKE - ASLEEP - REM - CORE - DEEP - type: "null" start: 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)))$ end: 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)))$ minutes: type: integer minimum: 0 maximum: 9007199254740991 required: - stage - start - end - minutes additionalProperties: false required: - night - source - start - end - asleepMinutes - inBedMinutes - awakeMinutes - awakenings - stages - segments additionalProperties: false required: - night - main - naps additionalProperties: false description: "One reconstructed sleep night: the main session's hypnogram segments + breakdown, plus same-wake-day naps surfaced separately." 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 - COMPUTED - WHOOP - FITBIT 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 - COMPUTED - WHOOP - FITBIT externalId: anyOf: - type: string - type: "null" metadata: anyOf: - {} - type: "null" route: anyOf: - $ref: "#/components/schemas/WorkoutRouteGeometry" - type: "null" samples: anyOf: - $ref: "#/components/schemas/WorkoutHrSeries" - type: "null" canonicalId: type: string required: - id - sportType - startedAt - endedAt - durationSec - distanceM - activeEnergyKcal - avgHr - maxHr - minHr - stepCount - elevationM - pauseDurationSec - source - externalId - metadata - route - samples - canonicalId additionalProperties: false WorkoutRouteGeometry: type: object properties: geometry: {} sampleTimestamps: anyOf: - type: array items: type: string - type: "null" required: - geometry - sampleTimestamps additionalProperties: false WorkoutHrSeries: type: object properties: sampleCount: type: integer minimum: 0 maximum: 9007199254740991 samples: {} required: - sampleCount - samples 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. unitsPerDose: type: number description: v1.16.10 — inventory units one dose consumes (e.g. 2 tablets of 2 mg for a 4 mg dose). v1.16.12 — may be a split-pill fraction (¼ / ⅓ / ½ / ⅔ / ¾); thirds carry as ≈0.3333 / 0.6667. Default 1. The intake consumption hook decrements this many units per taken dose; dose-derived readouts divide unit counts by it. active: type: boolean notificationsEnabled: type: boolean liveActivityEnabled: type: boolean description: v1.7.0 iOS Live Activity opt-in for this medication's reminders. Default false. The iOS client owns the ActivityKit lifecycle; the server only stores + echoes the flag. criticalAlarmEnabled: type: boolean description: v1.7.0 iOS 26 AlarmKit critical-reminder opt-in. Default false. Critical alarms bypass the device mute switch / Focus; the server stores the preference only. atcCode: anyOf: - type: string - type: "null" description: v1.9.0 optional WHO ATC classification code (active-substance class, e.g. `A10BX10`). User/clinician-asserted; never machine-guessed. Emitted on the FHIR `medicationCodeableConcept` under `http://www.whocc.no/atc`. NULL = no code captured. rxNormCode: anyOf: - type: string - type: "null" description: v1.9.0 optional RxNorm RxCUI (numeric, US identifier, e.g. `2601723`). Secondary FHIR coding under `http://www.nlm.nih.gov/research/umls/rxnorm`, alongside any ATC code. NULL = no code captured. pausedAt: anyOf: - type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" snoozedUntil: anyOf: - type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" nextDueAt: anyOf: - type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: "v1.7.0 server-computed next due instant across all the medication's schedules (earliest `nextOccurrenceAfter`). Read-only — computed, not stored. NULL when no schedule has an upcoming slot (paused, one-shot in the past, `endsOn` crossed, every schedule PRN). v1.16.4 — when an unresolved slot's anchor has passed but the catch-up band is still open (`anchor < now <= overdueEnd`, current schedule era), this carries THAT slot (a past instant) with `nextDueOverdue: true` instead of jumping to the next future slot. The list GET is cached 60 s, so a 60 s staleness is accepted." nextDueOverdue: type: boolean description: "v1.16.4 — true when `nextDueAt` is an OPEN overdue slot: its anchor has passed, `now` is still inside the slot's catch-up band, and no taken / skipped / auto-missed row resolves it. False for a regular future next-due (and when `nextDueAt` is null). Read-only — computed, not stored." 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. asNeeded: type: boolean description: "v1.16.11 as-needed (PRN) flag. When true the medication carries ZERO schedules (the write routes 422 on any schedule entry alongside the flag): it is never due (`nextDueAt` stays null), never reminded, and excluded from every compliance rate/streak — but intakes still log as ad-hoc rows, inventory still consumes per `unitsPerDose`, and the history renders. Stays active indefinitely. Mutually exclusive with `oneShot`." 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 ACTIONED intake events for today (user-local day window): rows with a recorded `takenAt` or an explicit skip. Pending projector-minted rows do not count — the card overdue-pill suppression compares this against the passed-dose count, and a pending mint must not read as covered." stockUnitsRemaining: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: v1.16.10 — usable inventory units left across the medication's containers (sum of `unitsRemaining` over ACTIVE / IN_USE items with units left). NULL = inventory tracking off (no items ever registered); 0 = tracking on, supply ran out. Read-only — aggregated, not stored. stockDosesRemaining: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: "v1.16.10 — dose-derived stock: `floor(stockUnitsRemaining / unitsPerDose)`, where `unitsPerDose` may be a fraction (½ tablet ⇒ twice the doses). Stays a whole-dose count. NULL when inventory tracking is off. Drives the table view's Bestand column. Read-only — aggregated, not stored." required: - id - name - dose - treatmentClass - dosesPerUnit - unitsPerDose - active - notificationsEnabled - liveActivityEnabled - criticalAlarmEnabled - atcCode - rxNormCode - pausedAt - snoozedUntil - nextDueAt - nextDueOverdue - startsOn - endsOn - oneShot - asNeeded - createdAt - updatedAt - schedules - category - lastTakenAt - todayEventCount - stockUnitsRemaining - stockDosesRemaining additionalProperties: false description: List-row variant of the medication resource enriched with the joined `category`, `lastTakenAt`, `todayEventCount`, and the v1.16.10 aggregated stock fields (`stockUnitsRemaining`, `stockDosesRemaining`) the dashboard + iOS client consume. The base medication fields (`id`, `name`, `dose`, `treatmentClass`, `dosesPerUnit`, `active`, `notificationsEnabled`, `pausedAt`, `snoozedUntil`, `startsOn`, `endsOn`, `oneShot`, `createdAt`, `updatedAt`, `schedules`) are inlined; see the `Medication` component for their semantics. MedicationCategory: type: string enum: - BLOOD_PRESSURE - VITAMIN - SUPPLEMENT - PAIN_RELIEF - ALLERGY - DIGESTIVE - THYROID - HORMONE - SKIN - SLEEP_AID - DIABETES - ANTIBIOTIC - OTHER description: Clinical taxonomy stored in the `medication_categories` side-table. Orthogonal to `MedicationTreatmentClass`. CreateMedicationResponse: type: object properties: data: $ref: "#/components/schemas/MedicationDetail" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MedicationDetail: type: object properties: id: type: string name: type: string dose: type: string treatmentClass: $ref: "#/components/schemas/MedicationTreatmentClass" dosesPerUnit: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: Doses per pen / vial for inventory tracking. NULL = inventory tracking off. unitsPerDose: type: number description: v1.16.10 — inventory units one dose consumes (e.g. 2 tablets of 2 mg for a 4 mg dose). v1.16.12 — may be a split-pill fraction (¼ / ⅓ / ½ / ⅔ / ¾); thirds carry as ≈0.3333 / 0.6667. Default 1. The intake consumption hook decrements this many units per taken dose; dose-derived readouts divide unit counts by it. active: type: boolean notificationsEnabled: type: boolean liveActivityEnabled: type: boolean description: v1.7.0 iOS Live Activity opt-in for this medication's reminders. Default false. The iOS client owns the ActivityKit lifecycle; the server only stores + echoes the flag. criticalAlarmEnabled: type: boolean description: v1.7.0 iOS 26 AlarmKit critical-reminder opt-in. Default false. Critical alarms bypass the device mute switch / Focus; the server stores the preference only. atcCode: anyOf: - type: string - type: "null" description: v1.9.0 optional WHO ATC classification code (active-substance class, e.g. `A10BX10`). User/clinician-asserted; never machine-guessed. Emitted on the FHIR `medicationCodeableConcept` under `http://www.whocc.no/atc`. NULL = no code captured. rxNormCode: anyOf: - type: string - type: "null" description: v1.9.0 optional RxNorm RxCUI (numeric, US identifier, e.g. `2601723`). Secondary FHIR coding under `http://www.nlm.nih.gov/research/umls/rxnorm`, alongside any ATC code. NULL = no code captured. pausedAt: anyOf: - type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" snoozedUntil: anyOf: - type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" nextDueAt: anyOf: - type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: "v1.7.0 server-computed next due instant across all the medication's schedules (earliest `nextOccurrenceAfter`). Read-only — computed, not stored. NULL when no schedule has an upcoming slot (paused, one-shot in the past, `endsOn` crossed, every schedule PRN). v1.16.4 — when an unresolved slot's anchor has passed but the catch-up band is still open (`anchor < now <= overdueEnd`, current schedule era), this carries THAT slot (a past instant) with `nextDueOverdue: true` instead of jumping to the next future slot. The list GET is cached 60 s, so a 60 s staleness is accepted." nextDueOverdue: type: boolean description: "v1.16.4 — true when `nextDueAt` is an OPEN overdue slot: its anchor has passed, `now` is still inside the slot's catch-up band, and no taken / skipped / auto-missed row resolves it. False for a regular future next-due (and when `nextDueAt` is null). Read-only — computed, not stored." 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. asNeeded: type: boolean description: "v1.16.11 as-needed (PRN) flag. When true the medication carries ZERO schedules (the write routes 422 on any schedule entry alongside the flag): it is never due (`nextDueAt` stays null), never reminded, and excluded from every compliance rate/streak — but intakes still log as ad-hoc rows, inventory still consumes per `unitsPerDose`, and the history renders. Stays active indefinitely. Mutually exclusive with `oneShot`." 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 - unitsPerDose - active - notificationsEnabled - liveActivityEnabled - criticalAlarmEnabled - atcCode - rxNormCode - pausedAt - snoozedUntil - nextDueAt - nextDueOverdue - startsOn - endsOn - oneShot - asNeeded - 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. MedicationListLayoutResponse: type: object properties: data: $ref: "#/components/schemas/MedicationListLayoutOutput" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MedicationListLayoutSaved: type: object properties: data: $ref: "#/components/schemas/MedicationListLayoutOutput" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MedicationListLayoutReset: type: object properties: data: $ref: "#/components/schemas/MedicationListLayoutOutput" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false 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 ListMedicationInventoryResponse: type: object properties: data: type: object properties: items: type: array items: $ref: "#/components/schemas/MedicationInventoryItem" meta: type: object properties: total: type: integer minimum: 0 maximum: 9007199254740991 required: - total additionalProperties: false required: - items - meta additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MedicationInventoryItem: type: object properties: id: type: string userId: type: string medicationId: type: string state: type: string enum: - ACTIVE - IN_USE - EXPIRED - USED_UP description: Container lifecycle state. ACTIVE = unopened; IN_USE = opened (30-day in-use clock running); EXPIRED = printed expiry or in-use window lapsed with units left; USED_UP = drained (terminal). containerType: type: string enum: - PEN - AMPOULE - BLISTER - INHALER - BOTTLE - OTHER description: Kind of physical container (PEN / AMPOULE / BLISTER / INHALER / BOTTLE / OTHER). Display-level classification; defaults to OTHER. unitsTotal: type: number description: Units the container shipped with (tablets / ampoules / puffs; 1–1000). v1.16.12 — fractional, so a split-pill remainder reads cleanly. Dose-derived readouts divide by the medication's `unitsPerDose`. unitsRemaining: type: number description: Units left in the container. v1.16.12 — fractional (a ½-tablet dose leaves 29.5 of 30). Decremented by the intake consumption hook (FEFO with spillover across containers); refunded when a taken dose is skipped, edited away, or deleted. firstUseAt: 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: Instant the container was first used. NULL until opened; starts the 30-day in-use clock. expiresAt: 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: Persisted MIN(firstUseAt + 30 days, printedExpiry). NULL when neither clock has started. printedExpiry: 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" purchasedAt: 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" notes: 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)))$ updatedAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - userId - medicationId - state - containerType - unitsTotal - unitsRemaining - firstUseAt - expiresAt - printedExpiry - purchasedAt - notes - createdAt - updatedAt additionalProperties: false description: One supply container (pen / blister pack / bottle) of a medication. Counts UNITS — the medication's `unitsPerDose` maps units to doses. The intake write paths consume from the open container first, then first-expiry-first-out over unopened stock. CreateMedicationInventoryItemResponse: type: object properties: data: $ref: "#/components/schemas/MedicationInventoryItem" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false UpdateMedicationInventoryItemResponse: type: object properties: data: $ref: "#/components/schemas/MedicationInventoryItem" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DeleteMedicationInventoryItemResponse: type: object properties: data: type: object properties: id: type: string deleted: type: boolean required: - id - deleted additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MedicationExtractResponse: type: object properties: data: type: object properties: name: type: string minLength: 1 maxLength: 100 dose: type: string minLength: 1 maxLength: 50 doseUnit: type: string enum: - mg - ml - iu - tablets - drops - puffs - sprays cadenceKind: type: string enum: - daily - weekdays - everyNWeeks - monthly - everyNMonths - yearly - rolling - oneShot weekdays: maxItems: 7 type: array items: type: string enum: - MO - TU - WE - TH - FR - SA - SU intervalWeeks: type: integer minimum: 1 maximum: 52 intervalMonths: type: integer minimum: 1 maximum: 12 dayOfMonth: type: integer minimum: 1 maximum: 31 rollingIntervalDays: type: integer minimum: 1 maximum: 365 timesOfDay: maxItems: 12 type: array items: type: string pattern: ^([01]\d|2[0-3]):[0-5]\d$ startsOn: type: string pattern: ^\d{4}-\d{2}-\d{2}$ endsOn: type: string pattern: ^\d{4}-\d{2}-\d{2}$ oneShot: type: boolean confidence: type: object propertyNames: type: string additionalProperties: type: number minimum: 0 maximum: 1 additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false GetMedicationCadenceResponse: type: object properties: data: $ref: "#/components/schemas/MedicationCadenceResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MedicationCadenceResponse: type: object properties: windowDays: type: integer exclusiveMinimum: 0 maximum: 9007199254740991 anchorIso: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ next: anyOf: - type: object properties: windowStart: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ windowEnd: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ scheduleIndex: type: integer minimum: 0 maximum: 9007199254740991 required: - windowStart - windowEnd - scheduleIndex additionalProperties: false - type: "null" chips: $ref: "#/components/schemas/MedicationCadenceChips" timeline: type: array items: $ref: "#/components/schemas/MedicationCadenceTimelinePoint" required: - windowDays - anchorIso - next - chips - timeline additionalProperties: false description: Cadence + compliance read for a single medication. `next` is the upcoming-dose envelope (null when the course has ended or the rolling clock has no pinning intake yet); `timeline` walks the requested `windowDays` worth of slots in ascending time order. MedicationCadenceChips: type: object properties: adherenceRate: type: number streakDays: type: integer minimum: 0 maximum: 9007199254740991 expectedSlots: type: integer minimum: 0 maximum: 9007199254740991 actualDoses: type: integer minimum: 0 maximum: 9007199254740991 required: - adherenceRate - streakDays - expectedSlots - actualDoses additionalProperties: false description: Four compliance summary values for the medication detail page chip row. MedicationCadenceTimelinePoint: type: object properties: day: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ windowStart: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ windowEnd: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ scheduleIndex: type: integer minimum: 0 maximum: 9007199254740991 status: type: string required: - day - windowStart - windowEnd - scheduleIndex - status additionalProperties: false description: One expected-vs-actual dose slot for the cadence timeline chart. `status` is one of `taken | skipped | missed | pending | future` and drives the chip colour. ListMedicationComplianceResponse: type: object properties: data: type: array items: $ref: "#/components/schemas/MedicationComplianceSummaryEntry" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MedicationComplianceSummaryEntry: type: object properties: medicationId: type: string compliance7: $ref: "#/components/schemas/ComplianceResult" compliance30: $ref: "#/components/schemas/ComplianceResult" complianceDisplay: $ref: "#/components/schemas/ComplianceDisplay" required: - medicationId - compliance7 - compliance30 - complianceDisplay additionalProperties: false description: "Compact per-medication adherence row for the batched card read: the 7-/30-day summaries plus the cadence-scaled display block. The per-day `dailyCompliance` grid is NOT on this shape — read the per-medication `/compliance` endpoint for the history glyph track." ComplianceResult: type: object properties: totalExpected: type: integer minimum: 0 maximum: 9007199254740991 description: "Full denominator over the window: `taken + skipped + missed`. Cadence-aware and clamped to the medication's `createdAt`." taken: type: integer minimum: 0 maximum: 9007199254740991 skipped: type: integer minimum: 0 maximum: 9007199254740991 description: Doses the user explicitly skipped — excluded from the `rate` denominator. missed: type: integer minimum: 0 maximum: 9007199254740991 rate: type: integer minimum: 0 maximum: 100 description: Adherence percentage `round(taken / (taken + missed) * 100)` — `skipped` is excluded from the denominator. streak: type: integer minimum: 0 maximum: 9007199254740991 description: Consecutive days with every due dose taken. required: - totalExpected - taken - skipped - missed - rate - streak additionalProperties: false description: Rolling-window adherence summary. `compliance30` is the authoritative 'last 30 days, taken vs expected' read — clients should display `rate` and use `totalExpected` as the denominator rather than re-deriving it from the daily map. ComplianceDisplay: type: object properties: shortDays: type: integer exclusiveMinimum: 0 maximum: 9007199254740991 longDays: type: integer exclusiveMinimum: 0 maximum: 9007199254740991 expectedShort: type: integer minimum: 0 maximum: 9007199254740991 expectedLong: type: integer minimum: 0 maximum: 9007199254740991 minStableDoses: type: integer minimum: 0 maximum: 9007199254740991 short: type: object properties: rate: type: integer minimum: 0 maximum: 100 taken: type: integer minimum: 0 maximum: 9007199254740991 expected: type: integer minimum: 0 maximum: 9007199254740991 description: v1.15.9 rate denominator over the short window (`taken + missed`); user skips are excluded. Render `taken / expected · rate%`. missed: type: integer minimum: 0 maximum: 9007199254740991 description: v1.15.9 doses counted against the rate over the short window — includes forgotten doses the auto-miss cron flipped. streak: type: integer minimum: 0 maximum: 9007199254740991 required: - rate - taken - expected - missed - streak additionalProperties: false long: type: object properties: rate: type: integer minimum: 0 maximum: 100 taken: type: integer minimum: 0 maximum: 9007199254740991 expected: type: integer minimum: 0 maximum: 9007199254740991 description: v1.15.9 rate denominator over the long window (`taken + missed`). missed: type: integer minimum: 0 maximum: 9007199254740991 description: v1.15.9 doses counted against the rate over the long window. required: - rate - taken - expected - missed additionalProperties: false currentCycle: type: object properties: state: type: string enum: - on_track - due - missed - none description: "Open-cycle state, decoupled from the percentage rows: `on_track` = next dose not yet due; `due` = due now / in grace; `missed` = past grace with no logged intake (the only red state); `none` = no projected next dose (PRN / paused / ended)." nextDueAt: anyOf: - type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: The open cycle's due instant. Null when `state` is `none`. graceUntil: 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: End of the due slot's grace window. Null when `state` is `none`. hasClosedCycles: type: boolean description: False for a brand-new sparse med with zero closed dose cycles — the percentage rows are vacuous and the card should show a neutral 'not enough data yet' state. required: - state - nextDueAt - graceUntil - hasClosedCycles additionalProperties: false description: v1.13.x — the current (open) dose cycle, surfaced so a between-doses sparse med renders a neutral 'next dose in N days' line instead of a scary red 0%. The percentage rows above already exclude the open forward cycle from their denominator. currentDose: type: object properties: status: type: string enum: - upcoming - on_time_window - overdue - missed - taken_on_time - taken_late - skipped description: Per-dose state of the open cycle, server-derived from the window model so the card renders the take-window (green) / overdue / heavily-overdue escalation from one authority. `upcoming` when no dose is open (PRN / paused / ended). targetAt: 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: Target instant of the open dose. Null when no dose is open. required: - status - targetAt additionalProperties: false description: v1.15.9 — the open dose's per-dose status + target, so the card highlights the actionable take-window green and escalates an overdue dose without re-deriving the window math client-side. required: - shortDays - longDays - expectedShort - expectedLong - minStableDoses - short - long - currentCycle - currentDose additionalProperties: false description: The two-row card block whose windows scale with dosing cadence (dense meds keep 7 / 30 days, sparse meds step both windows up). NOT the 30-day denominator — read `compliance30.totalExpected` for that. GetMedicationComplianceResponse: type: object properties: data: $ref: "#/components/schemas/MedicationComplianceResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MedicationComplianceResponse: type: object properties: compliance7: $ref: "#/components/schemas/ComplianceResult" compliance30: $ref: "#/components/schemas/ComplianceResult" dailyCompliance: type: object propertyNames: type: string additionalProperties: $ref: "#/components/schemas/DailyComplianceEntry" description: Flat per-day map keyed `YYYY-MM-DD` in the user timezone, one entry per day for up to 90 days back, clamped to the medication's `createdAt` (so a recently-created med has fewer entries). No weekly/monthly collapse — this is the raw daily grid. complianceDisplay: $ref: "#/components/schemas/ComplianceDisplay" required: - compliance7 - compliance30 - dailyCompliance - complianceDisplay additionalProperties: false description: Adherence read for a single medication. `compliance30` is the authoritative 30-day taken-vs-expected summary; `dailyCompliance` is the per-day grid for the history glyph track. The graded raw→week→month→year series used elsewhere for AI prompts does NOT apply here — this response is never downsampled. DailyComplianceEntry: type: object properties: expected: type: integer minimum: 0 maximum: 9007199254740991 description: Engine-computed due-slot count for the day. Equals `expectedCount`; kept for existing consumers. expectedCount: type: integer minimum: 0 maximum: 9007199254740991 description: True due-slot count for the day (additive field clients key off so they don't infer due-ness from `expected`). due: type: boolean description: "`expectedCount > 0`. Paint a per-day glyph as expected/missed ONLY when `due === true`; off-cadence / pre-creation / PRN days are not misses." taken: type: integer minimum: 0 maximum: 9007199254740991 skipped: type: integer minimum: 0 maximum: 9007199254740991 onTime: type: integer minimum: 0 maximum: 9007199254740991 description: Doses taken in the on-time band, including the `early` bucket (early counts as compliant). late: type: integer minimum: 0 maximum: 9007199254740991 veryLate: type: integer minimum: 0 maximum: 9007199254740991 early: description: Doses taken before the on-time band's grace start; already folded into `onTime`, surfaced separately for consumers that differentiate. type: integer minimum: 0 maximum: 9007199254740991 required: - expected - expectedCount - due - taken - skipped - onTime - late - veryLate additionalProperties: false description: Per-day compliance cell with the timing breakdown that drives the history glyph track. ListScheduleRevisionsResponse: type: object properties: data: type: object properties: currentSince: 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))$ description: "Instant the LIVE plan took over: the newest revision's `validUntil`, or the medication's `createdAt` when no era has been archived." revisions: type: array items: $ref: "#/components/schemas/MedicationScheduleRevision" description: Archived eras, newest first. required: - currentSince - revisions additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MedicationScheduleRevision: type: object properties: id: type: string validFrom: 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))$ description: Inclusive start instant of the era. validUntil: 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))$ description: Exclusive end instant of the era — the moment the next plan took over. source: type: string enum: - ARCHIVED - MANUAL description: Provenance. ARCHIVED = minted by the schedule-replace write path (immutable). MANUAL = user-entered pre-tracking era (deletable). entries: type: array items: $ref: "#/components/schemas/ScheduleRevisionEntry" required: - id - validFrom - validUntil - source - entries additionalProperties: false description: One archived schedule era covering `[validFrom, validUntil)`. The dose-history ledger, compliance tallies, and cadence chips mint past days against the era that was live then. ScheduleRevisionEntry: type: object properties: timesOfDay: type: array items: type: string description: Daily dose times (HH:mm, user local) the era ran at. label: anyOf: - type: string - type: "null" dose: anyOf: - type: string - type: "null" scheduleType: type: string description: Schedule-type discriminator of the archived row (SCHEDULED / PRN / CYCLIC). required: - timesOfDay - label - dose - scheduleType additionalProperties: false description: Display summary of one archived schedule row inside an era. The full snapshot (windows, rrule, doseWindows, …) stays server-side; this projection carries what the timeline renders. CreateScheduleRevisionResponse: type: object properties: data: $ref: "#/components/schemas/MedicationScheduleRevision" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false UpdateScheduleRevisionResponse: type: object properties: data: $ref: "#/components/schemas/MedicationScheduleRevision" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DeleteScheduleRevisionResponse: 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 GetAiProviderResponse: type: object properties: data: $ref: "#/components/schemas/AiProviderResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false AiProviderResponse: type: object properties: provider: anyOf: - type: string enum: - OPENAI - ANTHROPIC - LOCAL - CHATGPT_OAUTH - type: "null" description: The user's selected provider, or null when none is set. model: anyOf: - type: string - type: "null" baseUrl: anyOf: - type: string - type: "null" description: Custom base URL (LOCAL provider only); null otherwise. aiAvailable: type: boolean description: True when ANY provider can serve this user — including the operator's server-managed key when the user set none. iOS keys Coach visibility off this. managedBy: anyOf: - type: string enum: - user - local - server - type: "null" description: "Resolved provider origin: `user` (BYOK), `local` (self-hosted), `server` (operator key), or null when no provider is available." hasAnthropicKey: type: boolean anthropicKeyPreview: anyOf: - type: string - type: "null" description: "`...` + last 4 chars of the stored Anthropic key, or null." hasLocalKey: type: boolean hasOpenaiKey: type: boolean openaiKeyPreview: anyOf: - type: string - type: "null" description: "`...` + last 4 chars of the stored OpenAI key, or null." required: - provider - model - baseUrl - aiAvailable - managedBy - hasAnthropicKey - anthropicKeyPreview - hasLocalKey - hasOpenaiKey - openaiKeyPreview additionalProperties: false description: The calling user's AI-provider configuration plus the effective availability (`aiAvailable` + `managedBy`). Reports key presence + a masked preview only — never a plaintext key. GetCoachPrefsResponse: type: object properties: data: type: object properties: tone: default: warm type: string enum: - warm - neutral - concise verbosity: default: default type: string enum: - brief - default - detailed excludeMetrics: default: [] maxItems: 11 type: array items: type: string enum: - bp - weight - pulse - mood - compliance - hrv - sleep - resting_hr - steps - medications - anthropometrics showEvidenceByDefault: default: false type: boolean defaultWindow: default: allTime type: string enum: - last7days - last30days - last90days - allTime dataClusters: maxItems: 10 type: array items: type: string enum: - cardio - body - activity - workouts - sleep - mood - glucose - medication - mobility - environment required: - tone - verbosity - excludeMetrics - showEvidenceByDefault - defaultWindow additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false PutCoachPrefsResponse: type: object properties: data: type: object properties: tone: default: warm type: string enum: - warm - neutral - concise verbosity: default: default type: string enum: - brief - default - detailed excludeMetrics: default: [] maxItems: 11 type: array items: type: string enum: - bp - weight - pulse - mood - compliance - hrv - sleep - resting_hr - steps - medications - anthropometrics showEvidenceByDefault: default: false type: boolean defaultWindow: default: allTime type: string enum: - last7days - last30days - last90days - allTime dataClusters: maxItems: 10 type: array items: type: string enum: - cardio - body - activity - workouts - sleep - mood - glucose - medication - mobility - environment required: - tone - verbosity - excludeMetrics - showEvidenceByDefault - defaultWindow additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false GetDisableCoachResponse: type: object properties: data: $ref: "#/components/schemas/DisableCoachFlagOutput" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false PatchDisableCoachResponse: type: object properties: data: $ref: "#/components/schemas/DisableCoachFlagOutput" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false GetSourcePriorityResponse: type: object properties: data: type: object properties: steps: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT activeEnergy: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT walkingRunningDistance: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT flightsClimbed: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT sleep: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT weight: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT bloodPressure: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT pulse: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT bodyFat: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT bodyTemperature: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT spo2: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT hrv: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT restingHeartRate: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT vo2Max: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT skinTemperature: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT respiratoryRate: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT recovery: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT metricPriority: type: object properties: steps: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT activeEnergy: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT walkingRunningDistance: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT flightsClimbed: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT sleep: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT weight: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT bloodPressure: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT pulse: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT bodyFat: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT bodyTemperature: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT spo2: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT hrv: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT restingHeartRate: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT vo2Max: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT skinTemperature: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT respiratoryRate: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT recovery: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT 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 - COMPUTED - WHOOP - FITBIT activeEnergy: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT walkingRunningDistance: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT flightsClimbed: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT sleep: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT weight: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT bloodPressure: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT pulse: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT bodyFat: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT bodyTemperature: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT spo2: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT hrv: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT restingHeartRate: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT vo2Max: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT skinTemperature: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT respiratoryRate: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT recovery: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT metricPriority: type: object properties: steps: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT activeEnergy: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT walkingRunningDistance: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT flightsClimbed: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT sleep: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT weight: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT bloodPressure: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT pulse: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT bodyFat: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT bodyTemperature: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT spo2: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT hrv: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT restingHeartRate: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT vo2Max: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT skinTemperature: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT respiratoryRate: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT recovery: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT additionalProperties: false deviceTypePriority: type: object properties: default: maxItems: 8 type: array items: type: string enum: - watch - band - ring - phone - scale - other - unknown additionalProperties: maxItems: 8 type: array items: type: string enum: - watch - band - ring - phone - scale - other - unknown additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false UploadAvatarResponse: type: object properties: data: type: object properties: avatarUrl: type: string contentType: type: string enum: - image/jpeg - image/png - image/webp updatedAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - avatarUrl - contentType - updatedAt additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false GetProfileResponse: type: object properties: data: $ref: "#/components/schemas/ProfileResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false ProfileResponse: type: object properties: username: type: string displayName: anyOf: - type: string - type: "null" email: anyOf: - type: string - type: "null" dateOfBirth: 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" gender: anyOf: - type: string enum: - MALE - FEMALE - type: "null" heightCm: anyOf: - type: number - type: "null" locale: anyOf: - type: string - type: "null" timezone: type: string timeFormat: type: string enum: - AUTO - H12 - H24 description: Hour-cycle display preference. AUTO follows the locale convention, H12 forces AM/PM, H24 forces 24-hour. moodReminderEnabled: type: boolean fullName: anyOf: - type: string - type: "null" insurerName: anyOf: - type: string - type: "null" insurerIkNumber: anyOf: - type: string - type: "null" description: German insurer institution number (IKNR), 9 digits. insuranceNumber: anyOf: - type: string - type: "null" description: German KVNR, decrypted for the form prefill. required: - username - email - dateOfBirth - gender - heightCm - locale - timezone - timeFormat - moodReminderEnabled - fullName - insurerName - insurerIkNumber - insuranceNumber additionalProperties: false description: Flattened profile fields for the native client (GET). The KVNR is decrypted server-side; the IKNR (v1.8.6) is plaintext at rest. PatchProfileResponse: type: object properties: data: $ref: "#/components/schemas/ProfileUpdateResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false ProfileUpdateResponse: type: object properties: username: type: string displayName: anyOf: - type: string - type: "null" email: anyOf: - type: string - type: "null" dateOfBirth: 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" gender: anyOf: - type: string enum: - MALE - FEMALE - type: "null" heightCm: anyOf: - type: number - type: "null" locale: anyOf: - type: string - type: "null" timezone: type: string timeFormat: type: string enum: - AUTO - H12 - H24 description: Hour-cycle display preference. AUTO follows the locale convention, H12 forces AM/PM, H24 forces 24-hour. moodReminderEnabled: type: boolean fullName: anyOf: - type: string - type: "null" insurerName: anyOf: - type: string - type: "null" insurerIkNumber: anyOf: - type: string - type: "null" description: German insurer institution number (IKNR), 9 digits. hasInsuranceNumber: type: boolean description: Whether a KVNR is on file (the value itself is not echoed). required: - username - email - dateOfBirth - gender - heightCm - locale - timezone - timeFormat - moodReminderEnabled - fullName - insurerName - insurerIkNumber - hasInsuranceNumber additionalProperties: false description: Profile echo returned by the PATCH/PUT update path. Reports KVNR presence as a boolean rather than re-emitting the decrypted value. GetNotificationPrefsResponse: type: object properties: data: $ref: "#/components/schemas/NotificationPrefs" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false NotificationPrefs: type: object properties: medication: type: object properties: clientManaged: type: boolean description: True when the iOS app owns local medication reminders; the server-side MEDICATION_REMINDER APNs cron is suppressed. deliveryDefault: type: string enum: - server - client description: 'Roaming user-level delivery default. "client" implies clientManaged: true.' lowStockRunwayDays: anyOf: - type: integer minimum: 1 maximum: 60 - type: "null" description: Low-stock alert threshold as remaining runway days (1–60). null = alert off. Default 7. required: - clientManaged - deliveryDefault - lowStockRunwayDays additionalProperties: false mood: type: object properties: reminderHour: type: integer minimum: 0 maximum: 23 description: Local-time hour for the daily mood reminder. Default 22. required: - reminderHour additionalProperties: false cycle: type: object properties: clientManaged: type: boolean description: True when the iOS app owns local cycle reminders; the server-side cycle APNs cron is suppressed. required: - clientManaged additionalProperties: false coach: type: object properties: nudgesEnabled: type: boolean description: Master switch for the proactive Coach nudge cron. nudgeMedication: type: boolean description: Medication-group nudge triggers (compliance). nudgeVitals: type: boolean description: Vitals-group nudge triggers (bp / score / weight / sleep). nudgeRoutine: type: boolean description: Routine-group nudge triggers (measurement gap / self-context). nudgeFrequency: type: string enum: - weekly - biweekly description: "Nudge frequency cap: one per 7 or per 14 days." required: - nudgesEnabled - nudgeMedication - nudgeVitals - nudgeRoutine - nudgeFrequency additionalProperties: false required: - medication - mood - cycle - coach additionalProperties: false description: Fully-resolved per-user notification preferences. Every key is present — a null / drifted persisted row resolves to the documented defaults. PatchNotificationPrefsResponse: type: object properties: data: $ref: "#/components/schemas/NotificationPrefs" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false InsightsLayout: type: object properties: data: $ref: "#/components/schemas/InsightsLayoutBodyOutput" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false InsightsLayoutSaved: type: object properties: data: $ref: "#/components/schemas/InsightsLayoutBodyOutput" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false InsightsLayoutReset: type: object properties: data: $ref: "#/components/schemas/InsightsLayoutBodyOutput" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CoachFactsList: type: object properties: data: type: object properties: facts: type: array items: type: object properties: id: type: string category: type: string enum: - preference - condition - goal - constraint - context description: "App-side closed category: preference | condition | goal | constraint | context." text: type: string description: Decrypted fact text. confidence: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: 0..100 server-assigned extraction confidence. 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 - category - text - confidence - createdAt additionalProperties: false description: The caller's active facts, highest-confidence then newest first. Undecryptable rows are omitted. required: - facts additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CoachFactsCleared: type: object properties: data: type: object properties: cleared: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Number of active facts soft-deleted by the bulk clear. required: - cleared additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CoachFactDeleted: type: object properties: data: type: object properties: deleted: type: boolean description: True when a fact owned by the caller was soft-deleted; false for an unknown / cross-user / already-deleted id (idempotent no-op). required: - deleted additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false GetCoachAboutMeResponse: type: object properties: data: type: object properties: aboutMe: anyOf: - type: string - type: "null" conditions: anyOf: - type: string - type: "null" allergies: anyOf: - type: string - type: "null" coachFocus: anyOf: - type: string - type: "null" pendingQuestions: type: array items: type: string updatedAt: 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" maxChars: type: integer minimum: -9007199254740991 maximum: 9007199254740991 fieldMaxChars: type: integer minimum: -9007199254740991 maximum: 9007199254740991 required: - aboutMe - conditions - allergies - coachFocus - pendingQuestions - updatedAt - maxChars - fieldMaxChars additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false PutCoachAboutMeResponse: type: object properties: data: type: object properties: aboutMe: anyOf: - type: string - type: "null" conditions: anyOf: - type: string - type: "null" allergies: anyOf: - type: string - type: "null" coachFocus: anyOf: - type: string - type: "null" pendingQuestions: type: array items: type: string 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)))$ maxChars: type: integer minimum: -9007199254740991 maximum: 9007199254740991 fieldMaxChars: type: integer minimum: -9007199254740991 maximum: 9007199254740991 required: - aboutMe - conditions - allergies - coachFocus - pendingQuestions - updatedAt - maxChars - fieldMaxChars additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false GetCoachAboutMeQuestionsResponse: type: object properties: data: type: object properties: questions: type: array items: type: string required: - questions additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DeleteCoachAboutMeQuestionsResponse: type: object properties: data: type: object properties: questions: type: array items: type: string required: - questions additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false AdminInviteList: type: object properties: data: type: array items: 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)))$ expiresAt: 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)))$ usedAt: 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" revokedAt: 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" uses: type: integer minimum: -9007199254740991 maximum: 9007199254740991 maxUses: type: integer minimum: -9007199254740991 maximum: 9007199254740991 creator: anyOf: - type: object properties: id: type: string username: type: string required: - id - username additionalProperties: false - type: "null" consumer: anyOf: - type: object properties: id: type: string username: type: string required: - id - username additionalProperties: false - type: "null" redemptions: type: array items: type: object properties: id: type: string redeemedAt: 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)))$ user: anyOf: - type: object properties: id: type: string username: type: string email: anyOf: - type: string - type: "null" required: - id - username - email additionalProperties: false - type: "null" required: - id - redeemedAt - user additionalProperties: false required: - id - createdAt - expiresAt - usedAt - revokedAt - uses - maxUses - creator - consumer - redemptions additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false AdminInviteCreated: 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)))$ expiresAt: 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)))$ uses: type: integer minimum: -9007199254740991 maximum: 9007199254740991 maxUses: type: integer minimum: -9007199254740991 maximum: 9007199254740991 token: type: string description: Raw invite token (`hlv_<64hex>`). Shown exactly once — never persisted in plaintext. url: type: string description: Composed registration deep link (`/auth/register?invite=…`). required: - id - createdAt - expiresAt - uses - maxUses - token - url additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false AdminInviteRevoked: type: object properties: data: type: object properties: revoked: type: boolean required: - revoked 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. DashboardSnapshotResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/DashboardSnapshotResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DashboardSnapshotResponse: type: object properties: user: type: object properties: username: type: string timezone: type: string heightCm: anyOf: - type: number - type: "null" dateOfBirth: anyOf: - type: string - type: "null" gender: anyOf: - type: string enum: - MALE - FEMALE - type: "null" glucoseUnit: anyOf: - type: string - type: "null" onboardingTourCompleted: type: boolean greetingHour: type: integer minimum: -9007199254740991 maximum: 9007199254740991 required: - username - timezone - heightCm - dateOfBirth - gender - glucoseUnit - onboardingTourCompleted - greetingHour additionalProperties: false layout: type: object propertyNames: type: string additionalProperties: {} layoutCatalogue: type: array items: type: object properties: id: type: string visible: type: boolean order: type: integer minimum: -9007199254740991 maximum: 9007199254740991 required: - id - visible - order additionalProperties: false description: Full 27-id widget catalogue (server-known + iOS-only) with per-widget visibility + order. iOS-only ids are appended default-invisible. The web dashboard reads `layout`; this block is the cold-launch seed for the native client. metricStates: type: object propertyNames: type: string additionalProperties: type: object properties: value: type: number measuredAt: type: string unit: type: string required: - value - measuredAt - unit additionalProperties: false description: "Latest reading per chartable metric, keyed by the iOS `MetricKind` raw value (the non-obvious raws: `oxygenSaturation`, `totalBodyWater`, `heartRateVariability`, `bodyMassIndex`, `walkingAsymmetryPercentage`, `walkingDoubleSupportPercentage`, `environmentalAudioExposure`, `headphoneAudioExposure`, `activeEnergyBurned`). Each entry carries `value`, `measuredAt` (ISO8601), and the canonical `unit`. Types the user has never logged are omitted." tiles: type: object properties: summaries: type: object propertyNames: type: string additionalProperties: {} lastSeenByType: type: object propertyNames: type: string additionalProperties: {} mood: type: object properties: summary: anyOf: - type: object propertyNames: type: string additionalProperties: {} - type: "null" entries: type: array items: type: object properties: date: type: string score: type: number samples: type: integer minimum: -9007199254740991 maximum: 9007199254740991 required: - date - score - samples additionalProperties: false required: - summary - entries additionalProperties: false required: - summaries - lastSeenByType - mood additionalProperties: false extras: anyOf: - type: object properties: bpInTargetPct: anyOf: - type: number - type: "null" bpInTargetPct7d: anyOf: - type: number - type: "null" bpInTargetPct30d: anyOf: - type: number - type: "null" bpInTargetPctAllTime: anyOf: - type: number - type: "null" bpInTargetPctPriorMonth: anyOf: - type: number - type: "null" bpInTargetPctPriorYear: anyOf: - type: number - type: "null" glucoseByContext: type: object propertyNames: type: string additionalProperties: {} required: - bpInTargetPct - bpInTargetPct7d - bpInTargetPct30d - bpInTargetPctAllTime - bpInTargetPctPriorMonth - bpInTargetPctPriorYear - glucoseByContext additionalProperties: false - type: "null" medsToday: type: object properties: activeCount: type: integer minimum: -9007199254740991 maximum: 9007199254740991 scheduledToday: type: integer minimum: -9007199254740991 maximum: 9007199254740991 takenToday: type: integer minimum: -9007199254740991 maximum: 9007199254740991 skippedToday: type: integer minimum: -9007199254740991 maximum: 9007199254740991 nextDueAt: anyOf: - type: string - type: "null" nextDueOverdue: type: boolean nextDueMedicationName: anyOf: - type: string - type: "null" required: - activeCount - scheduledToday - takenToday - skippedToday - nextDueAt - nextDueOverdue - nextDueMedicationName additionalProperties: false description: "Today's medication block: active-medication count, today-window tally (scheduled / taken / skipped), and the earliest next-due slot across active medications. `nextDueOverdue: true` marks an OPEN overdue slot (anchor passed, still inside its catch-up band). Staleness contract: the snapshot is cache-served, so a `nextDueAt` in the past with `nextDueOverdue: false` means the anchor passed after the snapshot was built — render the plain day summary, never an overdue state." healthScore: anyOf: - type: object properties: score: type: integer minimum: -9007199254740991 maximum: 9007199254740991 band: type: string enum: - green - yellow - red delta: anyOf: - type: number - type: "null" required: - score - band - delta additionalProperties: false - type: "null" description: Personal health score summary (0..100 score, traffic-light band, week-over-week delta). Null on a rollup-coverage miss (it rides the thick phase alongside `extras`) and when no pillar is computable. Component breakdown is deliberately not serialised here. briefing: anyOf: - type: object propertyNames: type: string additionalProperties: {} - type: "null" briefingState: type: string enum: - ready - preparing - disabled - no-provider briefingUpdatedAt: anyOf: - type: string - type: "null" briefingStale: type: boolean description: True when `briefing` carries the last good (expired-TTL) briefing while a refresh is pending (`preparing`) or impossible (`no-provider`). Render the stale content with its `briefingUpdatedAt` timestamp instead of a blank tile. generatedAt: type: string required: - user - layout - layoutCatalogue - metricStates - tiles - extras - medsToday - healthScore - briefing - briefingState - briefingUpdatedAt - briefingStale - generatedAt additionalProperties: false description: "Unified above-the-fold dashboard payload. `tiles` always arrives (slim summaries + mood + resolved widget layout); `extras` (BD-in-target + per-context glucose) is null on a rollup-coverage miss so the strip never waits on the slowest read. `briefing` is lifted read-only from the pre-generated insight cache — never generated synchronously — and reports `ready` / `preparing` / `disabled` / `no-provider` via `briefingState` (`no-provider` = stale-or-missing cache with no AI provider configured anywhere, so no warm pass will fill it; stop polling and surface a connect-provider hint). A stale-but-parseable briefing is still delivered with `briefingStale: true`. `layoutCatalogue` (full 27-id widget catalogue) and `metricStates` (latest reading per metric, keyed by iOS `MetricKind` raw value) are additive cold-launch seeds for the native client; both derive in-process from data already fetched, adding no DB round-trip." InsightsComprehensiveResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/InsightsComprehensiveResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false InsightsComprehensiveResponse: type: object properties: summary: type: string recommendations: type: array items: type: object propertyNames: type: string additionalProperties: {} citations: type: array items: type: object propertyNames: type: string additionalProperties: {} warnings: type: array items: type: object propertyNames: type: string additionalProperties: {} dailyBriefing: anyOf: - type: object propertyNames: type: string additionalProperties: {} - type: "null" trendAnnotations: anyOf: - type: object propertyNames: type: string additionalProperties: {} - type: "null" storyboardAnnotations: type: array items: type: object propertyNames: type: string additionalProperties: {} metricSource: type: object propertyNames: type: string additionalProperties: {} revalidating: description: True when the body is served from last-good cache (stale-while-revalidate) while a fresh aggregation runs in the background. The client keeps polling on `revalidating` (bounded) so the open page converges on the fresh body. type: boolean 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. InsightsPregenerateResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/InsightsPregenerateResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false InsightsPregenerateResponse: type: object properties: queued: type: boolean description: True when the full warm was accepted and enqueued. locale: type: string enum: - de - en description: The locale the assessments are being warmed in. required: - queued - locale additionalProperties: false description: Acknowledgement that a full assessment warm was enqueued for the calling user. The generation runs out of band; the text lands in the read-only status routes. AnalyticsRangeResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/AnalyticsRangeResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false AnalyticsRangeResponse: type: object properties: range: type: string enum: - 7d - 30d - 90d - 1y description: The range that was read (echoes the request). windowDays: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Trailing-window length in days for the chosen range. granularity: type: string description: Rollup granularity the read resolved against (`DAY` / `WEEK` / `MONTH` / `YEAR`, or `none` on a coverage miss). current: description: Aggregate over the current window `[now-N, now)`. $ref: "#/components/schemas/AnalyticsWindowAggregate" previous: description: Aggregate over the previous comparable window `[now-2N, now-N)`. $ref: "#/components/schemas/AnalyticsWindowAggregate" delta: anyOf: - type: number - type: "null" description: "`current.mean - previous.mean`; null when either window has no data (never a misleading 0)." deltaPct: anyOf: - type: number - type: "null" description: "`delta / previous.mean` as a fraction (0.03 = +3 %); null when the prior window has no / zero mean (no divide-by-zero). The client shows 'no prior-period data' in that case." required: - range - windowDays - granularity - current - previous - delta - deltaPct additionalProperties: false description: Single-metric period-over-period aggregate. Reads the current and previous comparable windows from the WMY rollup tier and composes a count-weighted-mean delta. `count/min/max/mean/sum` are linearly composable across buckets; SD/slope/r² are intentionally excluded (not composable). AnalyticsWindowAggregate: type: object properties: count: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Reading count composed across buckets. min: anyOf: - type: number - type: "null" description: Window minimum; null when empty. max: anyOf: - type: number - type: "null" description: Window maximum; null when empty. mean: anyOf: - type: number - type: "null" description: Count-weighted mean across buckets; null when empty. sum: anyOf: - type: number - type: "null" description: Cumulative total for cumulative metrics (steps, energy, distance); null when no bucket carries a sum. required: - count - min - max - mean - sum additionalProperties: false BloodPressureStatusResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/InsightStatusResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false InsightStatusResponse: type: object properties: hasProvider: type: boolean description: False when the user has no usable AI provider — `text` then carries the generic no-key guidance. text: anyOf: - type: string - type: "null" description: The assessment narrative (plain text, rendered as React text children). Null while a first generation is preparing. cached: type: boolean description: True when `text` is served from cache (incl. last-good). updatedAt: 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: When the served assessment was generated; null when none. preparing: description: True when a first assessment is being generated out of band and no prior text exists yet — the client polls until it lands. type: boolean revalidating: description: True when `text` is served from last-good cache (stale-while-revalidate) while a fresh generation is in flight. The client keeps polling on `preparing || revalidating` (bounded) so the open card upgrades to the warmed assessment without a remount. type: boolean required: - hasProvider - text - cached - updatedAt additionalProperties: false description: "Specialised per-metric assessment envelope (blood-pressure, pulse, weight, bmi, mood). Identical shape to the generic metric-status card so the `InsightStatusCard` consumes it unchanged. Read-only + stale-while-revalidate: a cache miss warms a generation out of band and serves the last-good text meanwhile." PulseStatusResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/InsightStatusResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false WeightStatusResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/InsightStatusResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false BmiStatusResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/InsightStatusResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MoodStatusResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/InsightStatusResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MedicationComplianceStatusResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/MedicationComplianceStatusResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MedicationComplianceStatusResponse: type: object properties: hasProvider: type: boolean description: False when the user has no usable AI provider — `summary` then carries the generic no-key guidance. summary: anyOf: - type: string - type: "null" description: The overall compliance narrative (plain text). Null while a first generation is preparing. medications: type: array items: $ref: "#/components/schemas/MedicationComplianceStatusItem" description: Per-medication compliance notes. Empty while preparing or when no medication qualifies. cached: type: boolean description: True when the envelope is served from cache (incl. last-good). updatedAt: 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: When the served assessment was generated; null when none. preparing: description: True when a first assessment is being generated out of band and no prior summary exists yet — the client polls until it lands. type: boolean revalidating: description: True when the envelope is served from last-good cache (stale-while-revalidate) while a fresh generation is in flight. The client keeps polling on `preparing || revalidating` (bounded). type: boolean required: - hasProvider - summary - medications - cached - updatedAt additionalProperties: false description: Medication-compliance assessment envelope. Unlike the other six specialised cards it carries a `summary` plus a per-medication `text` array rather than a single `text` field. Read-only + stale-while-revalidate. MedicationComplianceStatusItem: type: object properties: medicationId: type: string description: The medication this note belongs to. text: type: string description: Per-medication compliance note (plain text). required: - medicationId - text additionalProperties: false MetricStatusResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/MetricStatusResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MetricStatusResponse: type: object properties: hasProvider: type: boolean description: False when the user has no usable AI provider — `text` then carries the generic no-key guidance. text: anyOf: - type: string - type: "null" description: The assessment narrative (plain text, rendered as React text children). Null while a first generation is preparing, or when the metric has insufficient data. cached: type: boolean description: True when `text` is served from cache (incl. last-good). updatedAt: 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: When the served assessment was generated; null when none. preparing: description: True when a first assessment is being generated out of band and no prior text exists yet — the client polls until it lands. type: boolean revalidating: description: True when `text` is served from last-good cache (stale-while-revalidate) while a fresh generation is in flight. The payload is otherwise terminal; the client keeps polling on `preparing || revalidating` (bounded) so the open card upgrades to the warmed assessment without a remount. type: boolean insufficient: description: True when the metric has no readings; no assessment is generated (no LLM call). The card shows its insufficient-data state. type: boolean required: - hasProvider - text - cached - updatedAt additionalProperties: false description: "Generic per-metric assessment envelope. Identical shape to the seven specialised `*-status` cards so the `InsightStatusCard` consumes it unchanged. Read-only + stale-while-revalidate: a cache miss warms a generation out of band and serves the last-good text meanwhile." DerivedMetricResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/DerivedMetricResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DerivedMetricResponse: type: object properties: metric: type: string enum: - VITALS_BASELINE - FITNESS_AGE - VASCULAR_AGE_DELTA - SLEEP_SCORE - READINESS - COINCIDENT_DEVIATION - HRV_BALANCE - BMI - RECOVERY_SCORE - STRESS_SCORE - STRAIN_SCORE - WRIST_TEMPERATURE_BASELINE - STAIR_ASCENT_SPEED_BASELINE - STAIR_DESCENT_SPEED_BASELINE - SIX_MINUTE_WALK_BAND - TRAJECTORY description: Echoes the requested derived-metric id (tags the union). status: type: string enum: - ok - insufficient description: "'ok' carries `value` + `confidence`; 'insufficient' carries `reason` and no value, but still carries `coverage` + `provenance` so the surface renders the same gating UI." value: anyOf: - type: object propertyNames: type: string additionalProperties: {} - type: "null" description: Metric-specific value object when status is 'ok' (e.g. { type, center, low, high, spread, sampleDays, k, series } for VITALS_BASELINE, where `series` is the trailing per-day mean values for the inline sparkline); null when 'insufficient'. coverage: $ref: "#/components/schemas/DerivedCoverage" confidence: anyOf: - $ref: "#/components/schemas/DerivedConfidence" - type: "null" description: Present when status is 'ok'; null when 'insufficient'. provenance: $ref: "#/components/schemas/DerivedProvenance" reason: anyOf: - type: string - type: "null" description: Why the value could not be produced; null when status is 'ok'. assessment: anyOf: - $ref: "#/components/schemas/DerivedAssessment" - type: "null" description: "v1.13.2 — short 'why is this score what it is' explanation, keyed to the SAME requested id (only for the per-score ids READINESS, SLEEP_SCORE, RECOVERY_SCORE, STRAIN_SCORE, STRESS_SCORE). Null for any other metric and whenever status !== 'ok'. Always non-empty when present: a deterministic text fills it (so provider-less accounts + the demo always get one) and warmer AI prose overrides it once cached." required: - metric - status - value - coverage - confidence - provenance - reason - assessment additionalProperties: false description: Flat `Derived` envelope for one derived wellness metric. Pure compute over the rollup tier (no LLM, no narrative). iOS decodes one stable shape and combines values across metrics; coverage/confidence/provenance let it render the same honesty chips. DerivedCoverage: type: object properties: requiredInputs: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Inputs the metric wants (its full input set). presentInputs: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Inputs actually present in the user's data. historyDays: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Distinct days of history backing the value (the gating floor). missing: type: array items: type: string description: Named inputs still missing — drives the 'track N more' nudge. required: - requiredInputs - presentInputs - historyDays - missing additionalProperties: false DerivedConfidence: type: object properties: score: type: number description: 0..100 confidence; feeds the shared coverage meter unchanged. band: type: string enum: - high - medium - low - draft description: Confidence band the meter renders. required: - score - band additionalProperties: false DerivedProvenance: type: object properties: inputs: type: array items: type: string description: Named inputs that actually backed the value. source: type: string enum: - DAY - WEEK - MONTH - YEAR - live - none description: Granularity the dominant read resolved against. 'live' = a coverage-miss live-SQL fallback; 'none' = no data backed the value. windowDays: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Trailing window the value summarises, in days. computedAt: 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)))$ description: Compute time (for cache-staleness + the 'as of' chip). required: - inputs - source - windowDays - computedAt additionalProperties: false DerivedAssessment: type: object properties: text: type: string description: Short, non-empty explanation of why the score sits where it does, referencing the score's contributors. source: type: string description: "'deterministic' for the always-on template text, or 'ai' when warmer provider prose has been cached." 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)))$ description: When the text was produced / last warmed. required: - text - source - updatedAt additionalProperties: false DerivedBatchResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/DerivedBatchResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DerivedBatchResponse: type: object properties: metrics: type: object propertyNames: type: string additionalProperties: $ref: "#/components/schemas/DerivedMetricResponse" description: Map keyed by the per-request token (`` or `:`). Each value is the same flat `Derived` envelope the single-metric route returns, so a client decodes one shape and reads back exactly the tokens it asked for. required: - metrics additionalProperties: false description: Batched derived-metric values. One request resolves the whole dashboard grid (the wellness scores + the derived re-frames + one baseline per vital) instead of N concurrent single-metric requests sharing the Prisma pool. Pure compute over the rollup tier — no LLM, no narrative, no cache table. CorrelationDiscoveryResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/CorrelationDiscoveryResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CorrelationDiscoveryResponse: type: object properties: discovered: type: array items: $ref: "#/components/schemas/DiscoveredCorrelation" description: Pairs surviving n ≥ 20, p < 0.05, AND the BH-FDR control. pairsTested: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Behaviour × outcome pairs assessed (for the honest footer). fdrQ: type: number description: The FDR target the surface used. minPairs: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Minimum paired-day count enforced per pair. required: - discovered - pairsTested - fdrQ - minPairs additionalProperties: false description: v1.10.0 — FDR-controlled correlation discovery over a curated behaviour × outcome matrix, lagged behaviour → next-day outcome. Only statistically-defensible pairs surface; descriptive, never causal. DiscoveredCorrelation: type: object properties: behaviour: type: string description: Behaviour channel (lag source), e.g. TIME_IN_DAYLIGHT, MOOD. outcome: type: string description: Outcome channel (lag target), e.g. SLEEP_DURATION, HEART_RATE_VARIABILITY. n: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Paired-day count after the day+1 lag join (≥ 20). r: type: number description: Pearson r over the lag-joined daily series. pValue: type: number description: Two-sided exact Student-t p-value (< 0.05). qValue: type: number description: Benjamini-Hochberg FDR-adjusted q-value (≤ the surface threshold). interpretation: type: string description: Conservative, descriptive interpretation — never causal. lagDays: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Lag in days applied (1). required: - behaviour - outcome - n - r - pValue - qValue - interpretation - lagDays additionalProperties: false MoodTagTreeEnvelope: type: object properties: data: type: object properties: categories: type: array items: $ref: "#/components/schemas/MoodTagCategoryDTO" required: - categories additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MoodTagCategoryDTO: type: object properties: key: type: string labelKey: anyOf: - type: string - type: "null" label: anyOf: - type: string - type: "null" icon: anyOf: - type: string - type: "null" custom: type: boolean tags: type: array items: $ref: "#/components/schemas/MoodTagDTO" required: - key - labelKey - label - icon - custom - tags additionalProperties: false description: "One group of the effective tree, already per-user ordered (layout applied server-side). `custom: true` = the caller's own group (render `label`); seeded groups resolve `labelKey`." MoodTagDTO: type: object properties: key: type: string labelKey: anyOf: - type: string - type: "null" label: anyOf: - type: string - type: "null" icon: anyOf: - type: string - type: "null" kind: type: string enum: - BINARY - RATED scaleMin: type: integer minimum: -9007199254740991 maximum: 9007199254740991 scaleMax: type: integer minimum: -9007199254740991 maximum: 9007199254740991 inverse: type: boolean custom: type: boolean hidden: description: Catalogue tags only, present when `include` contains `hidden`. type: boolean archived: description: Custom tags only, present when `include` contains `archived`. type: boolean usageCount: description: Live-entry link count for this user, present when `include` contains `usage`. type: integer minimum: -9007199254740991 maximum: 9007199254740991 required: - key - labelKey - label - icon - kind - scaleMin - scaleMax - inverse - custom additionalProperties: false description: One effective mood tag. Render `label` when `custom`, else resolve `labelKey` against the locale. MoodCustomTagCreatedEnvelope: type: object properties: data: type: object properties: key: type: string labelKey: type: "null" label: anyOf: - type: string - type: "null" icon: anyOf: - type: string - type: "null" kind: type: string enum: - BINARY - RATED scaleMin: type: integer minimum: -9007199254740991 maximum: 9007199254740991 scaleMax: type: integer minimum: -9007199254740991 maximum: 9007199254740991 inverse: type: boolean custom: type: boolean const: true required: - key - labelKey - label - icon - kind - scaleMin - scaleMax - inverse - custom additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MoodCustomTagPatchEnvelope: type: object properties: data: type: object properties: key: type: string labelKey: type: "null" label: anyOf: - type: string - type: "null" icon: anyOf: - type: string - type: "null" kind: type: string enum: - BINARY - RATED scaleMin: type: integer minimum: -9007199254740991 maximum: 9007199254740991 scaleMax: type: integer minimum: -9007199254740991 maximum: 9007199254740991 inverse: type: boolean custom: type: boolean const: true isActive: type: boolean required: - key - labelKey - label - icon - kind - scaleMin - scaleMax - inverse - custom - isActive additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MoodCustomTagDeleteEnvelope: type: object properties: data: type: object properties: key: type: string purged: type: boolean required: - key - purged additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MoodTagHiddenEnvelope: type: object properties: data: type: object properties: key: type: string hidden: type: boolean required: - key - hidden additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MoodTagGroupCreatedEnvelope: type: object properties: data: type: object properties: key: type: string labelKey: type: "null" label: anyOf: - type: string - type: "null" icon: anyOf: - type: string - type: "null" custom: type: boolean const: true required: - key - labelKey - label - icon - custom additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MoodTagGroupPatchEnvelope: type: object properties: data: type: object properties: key: type: string labelKey: type: "null" label: anyOf: - type: string - type: "null" icon: anyOf: - type: string - type: "null" custom: type: boolean const: true isActive: type: boolean required: - key - labelKey - label - icon - custom - isActive additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MoodTagGroupDeleteEnvelope: type: object properties: data: type: object properties: key: type: string purged: type: boolean rehomedCount: type: integer minimum: -9007199254740991 maximum: 9007199254740991 required: - key - purged - rehomedCount additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MoodTagLayoutGetEnvelope: type: object properties: data: $ref: "#/components/schemas/MoodTagLayoutResolved" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MoodTagLayoutResolved: type: object properties: groupOrder: type: array items: type: string placements: type: object propertyNames: type: string additionalProperties: type: array items: type: string required: - groupOrder - placements additionalProperties: false description: "The stored layout merged over defaults: `groupOrder` fully resolved against the user's effective category set, `placements` as stored." MoodTagLayoutPutEnvelope: type: object properties: data: $ref: "#/components/schemas/MoodTagLayoutResolved" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false GetReminderThresholdsResponse: type: object properties: data: $ref: "#/components/schemas/ReminderThresholdsResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false ReminderThresholdsResponse: type: object properties: lateMinutes: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Minutes after the dose anchor at which an open dose tiers 'late' (operator-level singleton; default 120). missedMinutes: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Minutes after the dose anchor at which an open dose tiers 'missed' (operator-level singleton; default 240). lowStockRunwayDays: anyOf: - type: integer minimum: 1 maximum: 60 - type: "null" description: Per-user low-stock alert threshold as remaining runway days (1–60). null = the alert is off. Default 7. Written via PATCH /api/auth/me/notification-prefs. required: - lateMinutes - missedMinutes - lowStockRunwayDays additionalProperties: false description: "Medication reminder thresholds: the operator-level late/missed minute marks plus the calling user's low-stock runway threshold (v1.16.11)." ConsentReceiptResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/ConsentReceiptResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false ConsentReceiptResponse: type: object properties: id: type: string receipt: type: object properties: id: type: string userId: type: string kind: type: string enum: - ai_full - ai_insights_only - ai_coach signedAt: type: string revokedAt: anyOf: - type: string - type: "null" createdAt: type: string required: - id - userId - kind - signedAt - revokedAt - createdAt additionalProperties: false required: - id - receipt additionalProperties: false description: The minted receipt. `artefact` is deliberately stripped from the response — it is opaque and only read at audit time directly from the DB. ConsentValidationErrorEnvelope: type: object properties: data: type: "null" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false WebConsentGrantResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/WebConsentGrantResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false WebConsentGrantResponse: type: object properties: minted: type: boolean description: True when a new `ai_full` receipt was minted; false when an active one already existed (idempotent no-op). kind: type: string const: ai_full required: - minted - kind additionalProperties: false description: Outcome of the idempotent web `ai_full` grant. Safe to call on every AI-settings mount. MedicationScheduleOutput: type: object properties: id: type: string medicationId: type: string windowStart: type: string description: Legacy single-time-of-intake (HH:mm, user local). Preserved for backwards compatibility; the new `timesOfDay` array supersedes it. windowEnd: type: string description: Legacy reminder-window upper bound (HH:mm). Used to derive the late-classification grace span when `reminderGraceMinutes` is null. label: anyOf: - type: string - type: "null" dose: anyOf: - type: string - type: "null" description: Per-schedule dose override. NULL means the schedule inherits `Medication.dose`. daysOfWeek: anyOf: - type: string - type: "null" description: Legacy persisted recurrence encoding (`null` | `1,3,5` | `i2;1,3,5`). v1.5 readers consult `rrule` first; the field is kept for pre-v1.5 rows. v1.6.0 drops the column. timesOfDay: type: array items: type: string description: v1.5 first-class points-in-time the dose is taken (HH:mm, user local). Backfilled to `[windowStart]` for every pre-v1.5 row. reminderGraceMinutes: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: Reminder grace window in minutes. NULL falls back to the legacy `windowEnd - windowStart` span. rrule: anyOf: - type: string - type: "null" description: RFC 5545 RRULE string (subset). Used for calendar-anchored cadences. **Mutually exclusive with `rollingIntervalDays`** — exactly one of the two is non-null on any v1.5+ schedule (or both are null on legacy rows that haven't been touched since the migration). rollingIntervalDays: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: Flexible-rolling interval in days, counted forward from the latest `MedicationIntakeEvent.takenAt`. **Mutually exclusive with `rrule`.** scheduleType: type: string enum: - SCHEDULED - PRN - CYCLIC description: v1.7.0 schedule-type discriminator. SCHEDULED = rrule / rolling / legacy cadence. PRN = as-needed (never projected, reminded, or counted in compliance expected; still loggable via the intake route). CYCLIC = N weeks on / M weeks off, gating whichever inner cadence the rrule / legacy fields describe. cyclicOnWeeks: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: v1.7.0 cyclic "on" weeks. Only meaningful when `scheduleType` is CYCLIC; null otherwise. cyclicOffWeeks: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: v1.7.0 cyclic "off" weeks. Only meaningful when `scheduleType` is CYCLIC; null otherwise. required: - id - medicationId - windowStart - windowEnd - label - dose - daysOfWeek - timesOfDay - reminderGraceMinutes - rrule - rollingIntervalDays - scheduleType - cyclicOnWeeks - cyclicOffWeeks additionalProperties: false description: Schedule entry attached to a medication. v1.5 promotes `timesOfDay` to first-class and introduces `rrule` (calendar-anchored cadences) and `rollingIntervalDays` (flexible-rolling cadences). The two recurrence primitives are mutually exclusive — enforced by the Zod refine on writes, the route layer, and a DB CHECK constraint (`medication_schedules_rrule_xor_rolling`). v1.7.0 adds `scheduleType` (SCHEDULED / PRN / CYCLIC) and the cyclic on/off-week fields. MedicationListLayoutOutput: type: object properties: version: type: number const: 1 view: description: Which presentation /medications renders in. Default "cards". Optional on PUT — when omitted the stored value is preserved (preserve-when-absent, like `heroVisible` on the dashboard layout). Always present on responses. type: string enum: - cards - table order: description: User-defined manual medication order (medication ids, first = top), shared by both views. Display-only — unknown / deleted ids are ignored at render time, never 422. Optional on PUT (preserve-when-absent); always present on responses. maxItems: 200 type: array items: type: string minLength: 1 maxLength: 64 required: - version additionalProperties: false description: "Per-user /medications presentation: the card/table view choice plus the manual medication order shared by both views. Mirrors the dashboard-widgets / insights-layout contract." DisableCoachFlagOutput: type: object properties: disableCoach: type: boolean required: - disableCoach additionalProperties: false description: Per-account Coach opt-out toggle (v1.4.47 W3). `true` hides the Coach FAB and short-circuits its API gates. InsightsLayoutBodyOutput: type: object properties: version: anyOf: - type: number const: 1 - type: number const: 2 sections: maxItems: 50 type: array items: type: object properties: id: type: string enum: - wellness-scores - daily-briefing - vitals - trends - period-review - cycle-summary - signals - rhythm-events visible: type: boolean order: type: integer minimum: 0 maximum: 99 required: - id - visible - order additionalProperties: false tiles: minItems: 1 maxItems: 50 type: array items: type: object properties: id: type: string enum: - overview - blood-pressure - pulse - oxygen - body-temperature - respiratory-rate - weight - bmi - body-water - bone-mass - fat-free-mass - fat-mass - muscle-mass - visceral-fat - lean-body-mass - steps - active-energy - workouts - flights-climbed - walking-distance - walking-steadiness - walking-heart-rate - walking-asymmetry - double-support-time - step-length - walking-speed - cardio-fitness - falls - six-minute-walk - stair-ascent-speed - stair-descent-speed - sleep - breathing-disturbances - resting-pulse - hrv - pulse-wave-velocity - vascular-age - cardio-recovery - environmental-audio - headphone-audio - audio-events - daylight - blood-glucose - skin-temperature - wrist-temperature - mood - medications - blutdruck - puls - sauerstoff - koerpertemperatur - gewicht - aktive-energie - schlaf - ruhepuls - stimmung - medikamente visible: type: boolean order: type: integer minimum: 0 maximum: 99 required: - id - visible - order additionalProperties: false required: - version additionalProperties: false description: "Per-user Insights layout (v2). `tiles` is the per-metric pill list (ordered, with a visibility flag); `sections` is the additive v2 list of the overview's big semantic blocks (wellness-scores, daily-briefing, vitals, trends, period-review, cycle-summary, signals, rhythm-events), each with order + visibility. `version` is the layout schema version: BOTH 1 and 2 are accepted on input (a pre-v2 iOS client still PUTs `version: 1`); the server always normalises to the canonical v2 blob before persisting, and GET responses always carry `version: 2`. Both arrays are optional on input — a client sending only `tiles` (the pre-v2 contract) still validates, and the server fills missing defaults; a v1 blob with no `sections` resolves forward with all sections default-visible. Tile ids are a closed enum: the canonical ids are English (matching the routed `/insights/` sub-pages). The legacy German tile ids (blutdruck, puls, sauerstoff, koerpertemperatur, gewicht, aktive-energie, schlaf, ruhepuls, stimmung, medikamente) remain accepted on input for backward compatibility and are normalised to their English equivalents before persisting; GET responses always carry the canonical English ids. Section ids are English-only. The legacy tile ids are deprecated and will be removed in a future major version." 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. unitsPerDose: type: number description: v1.16.10 — inventory units one dose consumes (e.g. 2 tablets of 2 mg for a 4 mg dose). v1.16.12 — may be a split-pill fraction (¼ / ⅓ / ½ / ⅔ / ¾); thirds carry as ≈0.3333 / 0.6667. Default 1. The intake consumption hook decrements this many units per taken dose; dose-derived readouts divide unit counts by it. active: type: boolean notificationsEnabled: type: boolean liveActivityEnabled: type: boolean description: v1.7.0 iOS Live Activity opt-in for this medication's reminders. Default false. The iOS client owns the ActivityKit lifecycle; the server only stores + echoes the flag. criticalAlarmEnabled: type: boolean description: v1.7.0 iOS 26 AlarmKit critical-reminder opt-in. Default false. Critical alarms bypass the device mute switch / Focus; the server stores the preference only. atcCode: anyOf: - type: string - type: "null" description: v1.9.0 optional WHO ATC classification code (active-substance class, e.g. `A10BX10`). User/clinician-asserted; never machine-guessed. Emitted on the FHIR `medicationCodeableConcept` under `http://www.whocc.no/atc`. NULL = no code captured. rxNormCode: anyOf: - type: string - type: "null" description: v1.9.0 optional RxNorm RxCUI (numeric, US identifier, e.g. `2601723`). Secondary FHIR coding under `http://www.nlm.nih.gov/research/umls/rxnorm`, alongside any ATC code. NULL = no code captured. pausedAt: anyOf: - type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" snoozedUntil: anyOf: - type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" nextDueAt: anyOf: - type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: "v1.7.0 server-computed next due instant across all the medication's schedules (earliest `nextOccurrenceAfter`). Read-only — computed, not stored. NULL when no schedule has an upcoming slot (paused, one-shot in the past, `endsOn` crossed, every schedule PRN). v1.16.4 — when an unresolved slot's anchor has passed but the catch-up band is still open (`anchor < now <= overdueEnd`, current schedule era), this carries THAT slot (a past instant) with `nextDueOverdue: true` instead of jumping to the next future slot. The list GET is cached 60 s, so a 60 s staleness is accepted." nextDueOverdue: type: boolean description: "v1.16.4 — true when `nextDueAt` is an OPEN overdue slot: its anchor has passed, `now` is still inside the slot's catch-up band, and no taken / skipped / auto-missed row resolves it. False for a regular future next-due (and when `nextDueAt` is null). Read-only — computed, not stored." 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. asNeeded: type: boolean description: "v1.16.11 as-needed (PRN) flag. When true the medication carries ZERO schedules (the write routes 422 on any schedule entry alongside the flag): it is never due (`nextDueAt` stays null), never reminded, and excluded from every compliance rate/streak — but intakes still log as ad-hoc rows, inventory still consumes per `unitsPerDose`, and the history renders. Stays active indefinitely. Mutually exclusive with `oneShot`." 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 - unitsPerDose - active - notificationsEnabled - liveActivityEnabled - criticalAlarmEnabled - atcCode - rxNormCode - pausedAt - snoozedUntil - nextDueAt - nextDueOverdue - startsOn - endsOn - oneShot - asNeeded - 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.