openapi: 3.1.0 info: title: HealthLog API version: 1.36.0 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: MeasurementReminders - name: Labs - name: Illness - name: Documents - name: Records - 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/measurement-categories: get: tags: - Meta summary: Measurement categorisation overlay description: "Returns the UI-side measurement categorisation as an HTTP contract: the ordered category list (id + i18n label key + order) plus the MeasurementType → category assignments. iOS reads this on cold start to drive the HealthKit permission picker (one consent screen per category) and caches it client-side (`Cache-Control: public, max-age=600`). Auth via cookie or Bearer (not admin); no PII." responses: "200": description: Categorisation overlay. content: application/json: schema: $ref: "#/components/schemas/MeasurementCategoriesEnvelope" "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: "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). `selection` is REQUIRED and carries the leaf inclusion list — membership is inclusion, absence is exclusion, and there is no server-side default. An unknown leaf id is a 422 that names it; the retired grouped `sections` shape is a 422 too, because accepting it would mean choosing a scope the caller did not express. 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 selection: type: object properties: v: type: number const: 2 leaves: maxItems: 91 type: array items: type: string minLength: 1 maxLength: 64 required: - v - leaves additionalProperties: false locale: type: string enum: - de - en - fr - es - it - pl practiceName: type: string maxLength: 120 includeCharts: type: boolean germanAtc: type: boolean required: - format - selection 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, together with a passphrase second factor and a `qrUrl` carrying that passphrase in the URL fragment; a leaked URL without the passphrase cannot open the record. The window and the `selection` are frozen write-once — there is no widen path. `selection` omitted means an empty scope, not a default one, and the `INSURANCE` leaf is refused with a 422 because the share view never decrypts the insurance number. An optional `documentIds` array (bounded ≤50) is validated as the caller's own live documents and frozen onto the link; a documents-only share (no report scope, non-empty `documentIds`) is valid. `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" selection: type: object properties: v: type: number const: 2 leaves: maxItems: 91 type: array items: type: string minLength: 1 maxLength: 64 required: - v - leaves additionalProperties: false documentIds: maxItems: 50 type: array items: type: string minLength: 1 maxLength: 64 documentOnly: 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/c/{token}/unlock: post: tags: - Export summary: Verify a share-link passphrase (v1.18.7, public) description: Anonymous, no-session passphrase gate for a protected share link. The raw path token plus the submitted passphrase are the only credentials. On success the server sets a short-lived (30 min), token-scoped, httpOnly cookie so `/c/` renders the record. Rate-limited (`share-unlock::`); every failure class (bad token, no passphrase set, wrong passphrase) answers one blunt 401. parameters: - in: path name: token schema: type: string required: true requestBody: required: true content: application/json: schema: type: object properties: passphrase: type: string minLength: 1 maxLength: 64 required: - passphrase additionalProperties: false responses: "200": description: Passphrase accepted; short-lived token-scoped unlock cookie set. content: application/json: schema: $ref: "#/components/schemas/ShareLinkUnlock" "401": description: Invalid passphrase (blunt — no failure detail leaked). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /c/{token}/d/{id}: get: tags: - Export summary: Serve a shared document blob (v1.28, public) description: "Anonymous, no-session share-scoped document serve route. The raw `hls_` share token in the path is the ONLY credential — never a session; it never elevates a normal authed route. Serves ONLY a document id present in the link's frozen `documentIds` set (a guessed / foreign / soft-deleted id → flat 404). A passphrase-protected link additionally requires the short-lived, token-scoped unlock cookie; revocation and expiry collapse to the same flat 404. The serving-class posture is identical to the owner `/api/documents/inbound/{id}/original` route: Class A (PDF/JPEG/PNG/WebP/GIF) inline with its true type, everything else `application/octet-stream` + `Content-Disposition: attachment` + `X-Content-Type-Options: nosniff`; `Cache-Control: private, no-store`. Images (JPEG/PNG/WebP) are EXIF/XMP/GPS-stripped at egress (the stored original is untouched); PDF/TIFF/HEIC/Office metadata passes through. Per-link rate-limited." parameters: - in: path name: token schema: type: string description: Raw `hls_` share token. required: true description: Raw `hls_` share token. - in: path name: id schema: type: string description: A document id on the link's frozen set. required: true description: A document id on the link's frozen set. responses: "200": description: The document bytes. Content-Type is the true stored type for Class A (inline) or `application/octet-stream` for Class B (attachment). content: application/octet-stream: schema: type: string format: binary "404": description: Flat 404 for every miss class (unknown/revoked/expired token, locked passphrase gate, or an id not on the frozen set). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "429": description: Per-link serve rate limit exceeded. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" /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, Coverage, Observation, MedicationStatement, MedicationAdministration), their `read` and `search-type` interactions, the `Patient/$everything` operation, and the `application/fhir+json` format. Every advertised interaction has a route. 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/Coverage: get: tags: - FHIR summary: FHIR R4 Coverage search description: "Read-only `searchset` Bundle of the caller's own Coverage resource, when an insurer is recorded and the insurance leaf is in the saved selection. 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/Patient/{id}: get: tags: - FHIR summary: FHIR R4 Patient read description: "Read-only `Patient` by id, out of the caller's own record. An id belonging to someone else is indistinguishable from one that never existed — both answer a `not-found` OperationOutcome. Auth: `fhir:read` scope; `userId` is narrowed from auth." parameters: - in: path name: id schema: type: string required: true responses: "200": description: The resource (application/fhir+json). content: application/fhir+json: schema: type: string format: binary "401": *a1 "404": description: OperationOutcome (application/fhir+json). content: application/fhir+json: schema: type: string format: binary "422": *a3 "429": *a4 /api/fhir/Coverage/{id}: get: tags: - FHIR summary: FHIR R4 Coverage read description: "Read-only `Coverage` by id, out of the caller's own record. An id belonging to someone else is indistinguishable from one that never existed — both answer a `not-found` OperationOutcome. Auth: `fhir:read` scope; `userId` is narrowed from auth." parameters: - in: path name: id schema: type: string required: true responses: "200": description: The resource (application/fhir+json). content: application/fhir+json: schema: type: string format: binary "401": *a1 "404": description: OperationOutcome (application/fhir+json). content: application/fhir+json: schema: type: string format: binary "422": *a3 "429": *a4 /api/fhir/Observation/{id}: get: tags: - FHIR summary: FHIR R4 Observation read description: "Read-only `Observation` by id, out of the caller's own record. An id belonging to someone else is indistinguishable from one that never existed — both answer a `not-found` OperationOutcome. Auth: `fhir:read` scope; `userId` is narrowed from auth." parameters: - in: path name: id schema: type: string required: true responses: "200": description: The resource (application/fhir+json). content: application/fhir+json: schema: type: string format: binary "401": *a1 "404": description: OperationOutcome (application/fhir+json). content: application/fhir+json: schema: type: string format: binary "422": *a3 "429": *a4 /api/fhir/MedicationStatement/{id}: get: tags: - FHIR summary: FHIR R4 MedicationStatement read description: "Read-only `MedicationStatement` by id, out of the caller's own record. An id belonging to someone else is indistinguishable from one that never existed — both answer a `not-found` OperationOutcome. Auth: `fhir:read` scope; `userId` is narrowed from auth." parameters: - in: path name: id schema: type: string required: true responses: "200": description: The resource (application/fhir+json). content: application/fhir+json: schema: type: string format: binary "401": *a1 "404": description: OperationOutcome (application/fhir+json). content: application/fhir+json: schema: type: string format: binary "422": *a3 "429": *a4 /api/fhir/MedicationAdministration/{id}: get: tags: - FHIR summary: FHIR R4 MedicationAdministration read description: "Read-only `MedicationAdministration` by id, out of the caller's own record. An id belonging to someone else is indistinguishable from one that never existed — both answer a `not-found` OperationOutcome. Auth: `fhir:read` scope; `userId` is narrowed from auth." parameters: - in: path name: id schema: type: string required: true responses: "200": description: The resource (application/fhir+json). content: application/fhir+json: schema: type: string format: binary "401": *a1 "404": description: OperationOutcome (application/fhir+json). content: application/fhir+json: schema: type: string format: binary "422": *a3 "429": *a4 /api/fhir/Patient/$everything: get: tags: - FHIR summary: FHIR R4 $everything (v1.11.0) description: "Read-only `Patient/$everything`: every resource the document bundle carries — Composition, Device, Patient, Coverage, Observations (including the cycle ones), MedicationStatements, MedicationAdministrations, Conditions, Encounters, AllergyIntolerances, FamilyMemberHistories and the DiagnosticReport — flattened into one `searchset` Bundle. Scoped to the owner's SAVED report selection; an account that never saved one gets nothing. 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/export/encrypted: post: tags: - Export summary: Download the full backup as a passphrase-encrypted archive description: v1.23. Same restore-compatible payload as the plaintext full backup, sealed into an `HLX1` archive (Argon2id-derived key + AES-256-GCM) under the supplied `passphrase`. Returns `application/octet-stream` (a `.hlx` file). When the account has a second factor enrolled, the call is step-up gated (`requireFreshMfa`, cookie-only) and returns 401 `auth.stepup.required` without a fresh factor; single-factor accounts use a normal session or Bearer. Shared `export:` rate bucket (10/h). The passphrase is never stored — a forgotten passphrase makes the archive unrecoverable. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/EncryptedExportRequest" responses: "200": description: Encrypted archive. `application/octet-stream`; the body is the `HLX1` binary (magic | version | Argon2 params | salt | iv | tag | ciphertext). content: application/octet-stream: schema: type: string format: binary "401": *a1 "422": *a3 "429": *a4 /api/auth/check-user: post: tags: - Auth summary: Resolve the next sign-in step for a typed identifier description: Given an email or username, returns which onboarding branch the iOS client should render plus the `hasPasskey` / `hasPassword` booleans so it can offer a Passkey affordance alongside a password field without a second round-trip. Anonymous surface; per-IP rate-limited (30 requests / 15 min). The response is the same whether or not the identifier matched, and the identifier is never echoed back. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CheckUserRequest" responses: "200": description: Discovery result. content: application/json: schema: $ref: "#/components/schemas/CheckUserEnvelope" "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. v1.23 — when the account has a confirmed second factor, the response carries no session/token. It returns HTTP 200 with `data: null, error: null` and `meta.mfaRequired: true` plus a single-use `meta.mfaTicket` and the `meta.methods` list. The client must POST the ticket + a code to `/api/auth/mfa/verify` to obtain the token bundle. Accounts without MFA are unchanged. 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 (token bundle / cookie) — or a second factor is required (`meta.mfaRequired`). content: application/json: schema: anyOf: - $ref: "#/components/schemas/LoginResponse" - $ref: "#/components/schemas/MfaRequiredResponse" "401": *a1 "403": description: Password login is disabled — the operator runs `OIDC_ONLY=true`. `meta.errorCode` = `oidc_only`; sign in through SSO instead. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/auth/mfa/verify: post: tags: - Auth summary: Complete a second-factor login challenge description: Presents the `mfaTicket` from the login `meta.mfaRequired` response plus a TOTP or recovery code. On success returns the SAME token bundle / session the password path issues, with the session marked second-factor-verified. The ticket is single-use; wrong codes are throttled and the ticket is burned at the attempt cap. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MfaVerifyRequest" responses: "200": description: Second factor verified — session + optional bearer issued. content: application/json: schema: $ref: "#/components/schemas/MfaVerifyResponse" "401": *a1 "422": *a3 "429": *a4 /api/auth/oidc/native/token: post: tags: - Auth summary: Exchange a native OIDC handoff code for the token bundle description: >- The cookie-less native leg of the OIDC SSO flow. The iOS app opens `GET /api/auth/oidc/login?client=native&code_challenge=` inside an `ASWebAuthenticationSession`; the callback returns a one-time handoff code on `healthlog://oidc-callback?code=hlh_…` (or an `mfa_ticket` when the account has a second factor, completed at /api/auth/mfa/verify). This endpoint exchanges the code + its PKCE `codeVerifier` for the SAME native bundle password login issues. Requires the native transport (no cookie, non-browser UA) — a browser is rejected. The code is single-use and expires in ~90 seconds; a replay of a consumed code revokes the pair the first exchange issued. A single generic 401 covers every invalid/expired/used/PKCE-mismatch case. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/OidcNativeTokenRequest" responses: "200": description: Handoff accepted — native access + refresh bundle. content: application/json: schema: $ref: "#/components/schemas/OidcNativeTokenResponse" "401": *a1 "422": *a3 "429": *a4 /api/auth/native/login: get: tags: - Auth summary: Start the first-party web-handoff login (browser navigation) description: "First leg of the native web-handoff login (iOS #65). On a self-hosted domain the iOS app opens this URL inside an `ASWebAuthenticationSession` with its PKCE `code_challenge`, so the login runs in the instance's real web origin (fixing password autofill + passkeys). The endpoint validates the challenge, sets a short-lived encrypted state cookie carrying the challenge + a DB-clock start time, and 302-redirects to `/auth/login?flow=native`. It writes no database rows. Every error 302-redirects to `healthlog://login-callback?error=` (a closed set: `invalid_request`, `rate_limited`) — an error carries no code or session. Anonymous; rate-limited 10 / 15 min." parameters: - in: query name: code_challenge schema: type: string minLength: 43 maxLength: 128 description: The app's PKCE S256 challenge (RFC 7636). `plain` is unsupported. required: true description: The app's PKCE S256 challenge (RFC 7636). `plain` is unsupported. responses: "302": description: Redirect to `/auth/login?flow=native` (state cookie set) on success, or to `healthlog://login-callback?error=` on failure. /api/auth/native/complete: get: tags: - Auth summary: Complete the web-handoff login and mint the code (browser navigation) description: "Second leg of the native web-handoff login (iOS #65). After an interactive login on `/auth/login?flow=native`, the page navigates the browser here as a top-level GET. The endpoint validates the web session against the database, enforces the freshness binding (`session.createdAt >= startedAt`, both DB-clock — a pre-existing session is refused), mints a single-use PKCE-locked handoff code, deletes the state cookie, destroys the scaffold web session, and 302-redirects to `healthlog://login-callback?code=hlh_…`. The token pair never rides the URL; only the opaque code does. The app never calls this directly. Every failure 302-redirects to `healthlog://login-callback?error=` (`invalid_state`, `no_session`, `stale_session`, `rate_limited`). Rate-limited 20 / 15 min." responses: "302": description: Redirect to `healthlog://login-callback?code=hlh_…` on success, or `…?error=` on any failure. /api/auth/native/token: post: tags: - Auth summary: Exchange a web-handoff code for the token bundle description: >- Third leg of the native web-handoff login (iOS #65). The app exchanges the one-time handoff code from `healthlog://login-callback?code=hlh_…` plus its PKCE `codeVerifier` for the SAME native bundle password login issues. Requires the native transport (no cookie, non-browser UA) — a browser is rejected. The code is single-use and expires in ~90 seconds; a replay of a consumed code revokes the pair the first exchange issued. The exchange is flow-gated: a code minted by the OIDC native leg is not redeemable here (and vice versa). A single generic 401 covers every invalid / expired / used / PKCE-mismatch / cross-flow case. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/NativeHandoffTokenRequest" responses: "200": description: Handoff accepted — native access + refresh bundle. content: application/json: schema: $ref: "#/components/schemas/NativeHandoffTokenResponse" "401": *a1 "422": *a3 "429": *a4 /api/auth/step-up/options: post: tags: - Auth summary: Begin a WebAuthn re-proof for a step-up elevation (Bearer only) description: "Returns SimpleWebAuthn assertion options plus a challenge id to present at POST /api/auth/step-up. `method: \"passkey\"` scopes the assertion to the account's primary passkeys; `method: \"webauthn\"` to its registered second-factor security keys. Bearer-only: a cookie session is refused, because a browser re-proves its factor at login and carries the result on its session row. Returns 409 when the account has no credential of the requested kind — fall back to another arm of the mint." requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/StepUpOptionsRequest" responses: "200": description: Assertion options issued. content: application/json: schema: $ref: "#/components/schemas/StepUpOptionsEnvelope" "401": *a1 "409": description: No passkey registered on this account. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/auth/step-up: post: tags: - Auth summary: Mint a single-use step-up elevation (Bearer only) description: >- Re-prove a factor and receive an opaque elevation that authorises exactly ONE second-factor-management call. WHICH factor you re-prove decides WHAT the elevation reaches. `password` reaches the same routes a plain cookie session reaches. `totp`, `webauthn`, and `passkey` additionally satisfy the fresh-factor routes — MFA disable, recovery-code regeneration, security-key removal — which is precisely the set of ceremonies for which the web marks a session second-factor-verified. The response carries `satisfiesFreshFactor` so a client can choose the right ceremony up front rather than discovering the refusal after spending a proof. A recovery code is NOT accepted here; an account that has lost its authenticator manages its second factor on the web. The elevation is bound to the exact token that minted it (another token, including the same account's, cannot redeem it), single-use, and valid for five minutes — the same window the cookie path uses. Present it as `X-Step-Up: hle_…` alongside the normal `Authorization: Bearer` header. It is spent only when the target route is about to act, so a 429, a 422, or a wrong code does not burn it. Presenting the token alone mints nothing: the body must carry a fresh factor proof. Every failure — wrong password, no password set on an SSO-provisioned account, an assertion for another account, a stale challenge, a replayed TOTP step — returns the same 401 with the same prose, and is audited server-side. Rate-limited per account (5 / 15 min) and per source address. Accepting routes (the complete set): POST /api/auth/me/mfa/totp/setup; POST /api/auth/me/mfa/totp/confirm; POST /api/auth/me/mfa/disable; POST /api/auth/me/mfa/recovery-codes/regenerate; POST /api/auth/me/mfa/webauthn/register/options; POST /api/auth/me/mfa/webauthn/register/verify; PATCH and DELETE /api/auth/me/mfa/webauthn/{id}. The last three of those and disable require a fresh-factor proof. GET /api/auth/me/mfa needs no elevation at all. Nothing else accepts one — admin endpoints stay cookie-only. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/StepUpMintRequest" responses: "200": description: Factor re-proved — elevation issued. content: application/json: schema: $ref: "#/components/schemas/StepUpMintEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/auth/me/mfa/totp/setup: post: tags: - Auth summary: Begin TOTP enrollment (cookie session or step-up elevation) description: Generates and stores a pending (encrypted) TOTP secret and returns the otpauth URI + Base32 secret. MFA is not active until /confirm. Accepts a cookie session, or a Bearer token presenting a single-use elevation from POST /api/auth/step-up in the `X-Step-Up` header; a Bearer token alone is still refused. responses: "200": description: Pending secret created. content: application/json: schema: $ref: "#/components/schemas/TotpSetupEnvelope" "401": *a1 "409": description: A second factor is already active on this account. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/auth/me/mfa/totp/confirm: post: tags: - Auth summary: Confirm TOTP enrollment (cookie session or step-up elevation) description: Verifies a code against the pending secret, activates the factor, and returns the one-time recovery codes. Accepts a cookie session, or a Bearer token presenting a single-use elevation from POST /api/auth/step-up in the `X-Step-Up` header; a Bearer token alone is still refused. requestBody: required: true content: application/json: schema: type: object properties: code: type: string pattern: ^\d{6}$ required: - code responses: "200": description: Factor activated — recovery codes returned once. content: application/json: schema: $ref: "#/components/schemas/TotpConfirmEnvelope" "401": *a1 "409": description: A second factor is already active, or enrollment was never started (no pending secret to confirm). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/auth/me/mfa/disable: post: tags: - Auth summary: Disable the second factor (step-up gated) description: Requires a fresh second-factor step-up AND a current TOTP or recovery code. Clears the secret and deletes recovery codes. Accepts a cookie session, or a Bearer token presenting a single-use elevation from POST /api/auth/step-up in the `X-Step-Up` header; a Bearer token alone is still refused. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MfaDisableRequest" responses: "200": description: Factor disabled. content: application/json: schema: $ref: "#/components/schemas/MfaDisableEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/auth/me/mfa/recovery-codes/regenerate: post: tags: - Auth summary: Regenerate recovery codes (step-up gated) description: Invalidates the entire prior recovery-code set and returns a fresh batch once. Step-up gated. Accepts a cookie session, or a Bearer token presenting a single-use elevation from POST /api/auth/step-up in the `X-Step-Up` header; a Bearer token alone is still refused. responses: "200": description: Fresh recovery codes issued. content: application/json: schema: $ref: "#/components/schemas/MfaRecoveryRegenEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/auth/me/mfa: get: tags: - Auth summary: Second-factor status (cookie session or Bearer token) description: Whether TOTP is active, how many recovery codes remain, and the registered WebAuthn security keys. Metadata only — no secret, no code, no public key, no credential id. Plain authentication (cookie session or cookie-equivalent token); unlike every mutation on this surface it needs no step-up elevation, because the payload carries no credential material. responses: "200": description: Second-factor status. content: application/json: schema: $ref: "#/components/schemas/MfaStatusEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/auth/me/mfa/webauthn/register/options: post: tags: - Auth summary: Begin registering a security key as a second factor description: Returns SimpleWebAuthn creation options + a challenge id. Accepts a cookie session, or a Bearer token presenting a single-use elevation from POST /api/auth/step-up in the `X-Step-Up` header; a Bearer token alone is still refused. responses: "200": description: Registration options issued. content: application/json: schema: $ref: "#/components/schemas/MfaWebauthnRegisterOptionsEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/auth/me/mfa/webauthn/register/verify: post: tags: - Auth summary: Finish registering a security key as a second factor description: Verifies the attestation against the user-bound challenge and stores the credential in the second-factor store (separate from primary passkeys). Accepts a cookie session, or a Bearer token presenting a single-use elevation from POST /api/auth/step-up in the `X-Step-Up` header; a Bearer token alone is still refused. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MfaWebauthnRegisterVerifyRequest" responses: "200": description: Security key registered. content: application/json: schema: $ref: "#/components/schemas/MfaWebauthnRegisterVerifyEnvelope" "400": description: Security key verification failed (bad attestation). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/auth/me/mfa/webauthn/{id}: patch: tags: - Auth summary: Rename a registered security key description: Rename a registered second-factor security key. Accepts a cookie session, or a Bearer token presenting a single-use elevation from POST /api/auth/step-up in the `X-Step-Up` header; a Bearer token alone is still refused. parameters: - in: path name: id schema: type: string required: true requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MfaWebauthnRenameRequest" responses: "200": description: Security key renamed. content: application/json: schema: $ref: "#/components/schemas/MfaWebauthnRenameEnvelope" "401": *a1 "404": description: No such security key for this account. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 delete: tags: - Auth summary: Remove a registered security key (step-up gated) description: Requires a fresh second-factor step-up. Accepts a cookie session, or a Bearer token presenting a single-use elevation from POST /api/auth/step-up in the `X-Step-Up` header; a Bearer token alone is still refused. parameters: - in: path name: id schema: type: string required: true responses: "200": description: Security key removed. content: application/json: schema: $ref: "#/components/schemas/MfaWebauthnRemoveEnvelope" "401": *a1 "404": description: No such security key for this account. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/auth/mfa/webauthn/verify/options: post: tags: - Auth summary: Begin a mid-login security-key assertion description: Presents the login `mfaTicket` and returns assertion options scoped to the password-identified user's registered security keys. Anonymous surface; rate-limited. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MfaWebauthnLoginOptionsRequest" responses: "200": description: Assertion options issued. content: application/json: schema: $ref: "#/components/schemas/MfaWebauthnLoginOptionsEnvelope" "401": *a1 "409": description: No security key is registered on the account — fall back to the TOTP / recovery-code challenge. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/auth/mfa/webauthn/verify: post: tags: - Auth summary: Complete a security-key second-factor login challenge description: Presents the login `mfaTicket` plus the assertion. On success returns the SAME token bundle / session the password path issues, with the session marked second-factor-verified. The ticket is single-use; failures are throttled and the ticket is burned at the attempt cap. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MfaWebauthnLoginVerifyRequest" responses: "200": description: Second factor verified — session + optional bearer issued. content: application/json: schema: $ref: "#/components/schemas/MfaWebauthnVerifyResponse" "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 "403": description: Passkey login is disabled — the operator runs `OIDC_ONLY=true`. `meta.errorCode` = `oidc_only`. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "404": description: The assertion resolved to a user row that no longer exists. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "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. When the request carried `revoke: true` the body is `{ revoked }` instead: the token family is invalidated and no new pair is minted." content: application/json: schema: anyOf: - $ref: "#/components/schemas/RefreshResponse" - $ref: "#/components/schemas/RefreshRevokeResponse" "401": *a1 "422": *a3 "429": *a4 /api/auth/passkeys: get: tags: - Auth summary: List the caller's registered passkeys description: The passkeys registered for primary sign-in, newest first. Distinct from the second-factor security keys in `GET /api/auth/2fa/status` (`webauthn`) — a passkey replaces the password, a security key is asserted after one. `lastUsedAt` is stamped on every verified assertion and is null only for a passkey that has never signed in. responses: "200": description: The caller's passkeys, newest first. content: application/json: schema: $ref: "#/components/schemas/PasskeyListEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/auth/me/sessions: get: tags: - Auth summary: List active web sessions description: "v1.23 — the user-facing active-session list (issue #64). One row per browser login with a coarse device label, masked IP, resolved location, sliding last-active time, and the current-session marker. Distinct from /api/auth/me/devices (notification devices)." responses: "200": description: Active sessions for the caller. content: application/json: schema: $ref: "#/components/schemas/SessionListEnvelope" "401": *a1 "422": *a3 "429": *a4 delete: tags: - Auth summary: Sign out everywhere else description: v1.23 — revokes every OTHER web session plus all native refresh tokens, keeping the caller's current session. API tokens are not touched (manage those under /settings/api-tokens). responses: "200": description: Other sessions revoked. content: application/json: schema: $ref: "#/components/schemas/SignOutEverywhereEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/auth/me/sessions/{id}: delete: tags: - Auth summary: Revoke a single web session description: v1.23 — revokes one session by id, scoped to the authenticated user (a foreign id returns 404, never another user's row). parameters: - in: path name: id schema: type: string required: true responses: "200": description: Session revoked. content: application/json: schema: $ref: "#/components/schemas/SessionRevokeEnvelope" "401": *a1 "404": description: Session not found or not owned by the caller. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/auth/me/trusted-devices: get: tags: - Auth summary: List trusted devices description: v1.23 — the 'remember this device' list. A trusted device skips the second factor for 30 days (the password is still required). Returns only an IP-free device label + lifecycle timestamps, never the token. responses: "200": description: Trusted devices for the caller. content: application/json: schema: $ref: "#/components/schemas/TrustedDeviceListEnvelope" "401": *a1 "422": *a3 "429": *a4 delete: tags: - Auth summary: Forget every trusted device description: v1.23 — revokes all trusted devices for the caller and clears the caller's own trusted-device cookie. responses: "200": description: All trusted devices revoked. content: application/json: schema: $ref: "#/components/schemas/TrustedDeviceRevokeAllEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/auth/me/trusted-devices/{id}: delete: tags: - Auth summary: Revoke a single trusted device description: v1.23 — revokes one trusted device by id, scoped to the authenticated user (a foreign id returns 404). parameters: - in: path name: id schema: type: string required: true responses: "200": description: Trusted device revoked. content: application/json: schema: $ref: "#/components/schemas/TrustedDeviceRevokeEnvelope" "401": *a1 "404": description: Trusted device not found or not owned by the caller. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/auth/me/security-activity: get: tags: - Auth summary: List recent account-security activity description: "v1.23 — the SHARED security-activity feed: the caller's recent auth + export + deletion audit events with timestamp, resolved location, and a host-masked IP. `limit` query param caps at 100 (default 50). Reuses the AuditLog store; no event detail bodies are surfaced." parameters: - in: query name: limit schema: description: Page size; defaults to 50, clamped to 100. type: integer minimum: 1 maximum: 100 description: Page size; defaults to 50, clamped to 100. responses: "200": description: Recent security events for the caller. content: application/json: schema: $ref: "#/components/schemas/SecurityActivityEnvelope" "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 `lastSyncedAt` checkpoint, which the batch ingest endpoints advance when a client push lands; this route is a read and writes nothing. 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-batch: get: tags: - Measurements summary: Batched daily series (dashboard fetch coalescing) description: Returns the rollup-backed daily series for every requested MeasurementType in ONE response, so the dashboard chart row does a single round-trip instead of one self-fetch per chart. Each per-type series is byte-identical with `GET /api/measurements?aggregate=daily&source=rollup` for the same window. SLEEP_DURATION is not served here (it rides the per-night reconstruction at `/api/measurements/series?kind=sleep`). Auth via cookie or Bearer. parameters: - in: query name: types schema: description: Comma-separated list of MeasurementType values (1..16). Each is read as a daily series through the rollup tier. type: string minLength: 1 required: true description: Comma-separated list of MeasurementType values (1..16). Each is read as a daily series through the rollup tier. - in: query name: from schema: description: Window start (ISO-8601 instant). type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: true description: Window start (ISO-8601 instant). - in: query name: to schema: description: Window end (ISO-8601 instant). type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: true description: Window end (ISO-8601 instant). responses: "200": description: Per-type daily series. content: application/json: schema: $ref: "#/components/schemas/GetMeasurementsSeriesBatchResponse" "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 - ANS_CHARGE - CARDIO_LOAD - SLEEP_SCORE - BODY_TEMPERATURE_DEVIATION - RESILIENCE - PHQ9_SCORE - GAD7_SCORE - GRIP_STRENGTH - PAIN_NRS - WAIST_CIRCUMFERENCE - WAIST_TO_HEIGHT - WHO5_SCORE - SCI_SCORE - 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 - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA - in: query name: valueMin schema: type: number - in: query name: valueMax schema: type: number - 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 (or a small array) description: Single ingest. The body may also be a bare ARRAY of the same objects — the mode the iOS client uses for a combined blood-pressure + pulse or dual-value glucose write — in which case `data` is the array of created rows in request order. Use `/api/measurements/batch` for Apple Health upload streams. requestBody: required: true content: application/json: schema: anyOf: - 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 - ANS_CHARGE - CARDIO_LOAD - SLEEP_SCORE - BODY_TEMPERATURE_DEVIATION - RESILIENCE - PHQ9_SCORE - GAD7_SCORE - GRIP_STRENGTH - PAIN_NRS - WAIST_CIRCUMFERENCE - WAIST_TO_HEIGHT - WHO5_SCORE - SCI_SCORE 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" symptomsPresent: type: boolean required: - type - value - measuredAt - type: array items: 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 - ANS_CHARGE - CARDIO_LOAD - SLEEP_SCORE - BODY_TEMPERATURE_DEVIATION - RESILIENCE - PHQ9_SCORE - GAD7_SCORE - GRIP_STRENGTH - PAIN_NRS - WAIST_CIRCUMFERENCE - WAIST_TO_HEIGHT - WHO5_SCORE - SCI_SCORE 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" symptomsPresent: type: boolean required: - type - value - measuredAt responses: "201": description: Created. A single resource for the object body; an array of resources, in request order, for the array body. content: application/json: schema: $ref: "#/components/schemas/CreateMeasurementResponse" "401": *a1 "409": description: A measurement with this data already exists — the `(userId, type, source, externalId)` dedup index rejected the write. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/measurements/{id}: get: tags: - Measurements summary: Measurement detail description: Single stored row, scoped to the caller. A row owned by another user surfaces as 404 (existence channel sealed). parameters: - in: path name: id schema: type: string required: true responses: "200": description: Measurement. content: application/json: schema: $ref: "#/components/schemas/GetMeasurementResponse" "401": *a1 "404": description: Measurement not found (or owned by another user). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 put: tags: - Measurements summary: Update a measurement description: Edits a manually entered row. Rows owned by a connected source are refused with 409 `measurement.update.server_owned_source`; a timestamp collision with an existing row is refused with 409 `measurement.duplicate_timestamp`. parameters: - in: path name: id schema: type: string required: true requestBody: required: true content: application/json: schema: type: object properties: 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: anyOf: - type: string maxLength: 200 - type: "null" responses: "200": description: Updated measurement. content: application/json: schema: $ref: "#/components/schemas/UpdateMeasurementResponse" "401": *a1 "404": description: Measurement not found (or owned by another user). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "409": description: "Refused: the row comes from a connected source (`meta.errorCode` = `measurement.update.server_owned_source`), or another row already carries this timestamp (`measurement.duplicate_timestamp`)." content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 delete: tags: - Measurements summary: Delete a measurement description: Soft-deletes the row (tombstone) so paired clients can reconcile the deletion. parameters: - in: path name: id schema: type: string required: true responses: "200": description: Deleted. content: application/json: schema: $ref: "#/components/schemas/DeleteMeasurementResponse" "401": *a1 "404": description: Measurement not found (or owned by another user). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "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 HealthKit-ingested measurement rows (source `APPLE_HEALTH`) whose externalId is in the request list. Only app-minted rows are eligible: an externalId that maps to an integration- or manually-sourced row (a colliding externalUUID) is a no-op, never a delete. 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 "403": &a5 description: 'Module disabled for this account: the user (or operator) has the module turned off. `meta.errorCode` = `module.disabled` and `meta.module` carries the disabled module key (e.g. "sleep"). Returned even for a valid Bearer token. Clients hide the whole module surface end-to-end rather than retry.' content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/sleep/rhythm: get: tags: - Measurements summary: Sleep rhythm — sleep-debt + chronotype (v1.17.0) description: "Returns the two server-authoritative sleep-timing signals the Sleep page + iOS render off the same canonical night reconstruction the Sleep Score uses: the outstanding `sleepDebt` balance over the rolling window (calm `partial` state under the night threshold) and MCTQ `chronotype` (MSF/MSFsc band + social jetlag, `learning` state until enough free-day nights exist). Free vs work nights default to weekend = free in the user's timezone (no work calendar). A read-only view over existing per-stage SLEEP_DURATION rows — no schema, no new measurement type." responses: "200": description: Sleep-debt + chronotype DTO. content: application/json: schema: $ref: "#/components/schemas/SleepRhythmResponse" "401": *a1 "403": *a5 "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 "403": *a5 "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. Additive enrichment fields (`hrSeries`, `zones`, `splits`, `sportContext`) are computed server-side. `aiInsight` is a pure read of a per-workout paragraph written by a background job when the workout landed; it is null for every workout that predates the feature or was re-synced, and reading this endpoint never generates one. `compact=1` (sent by the web client) drops the raw `samples.samples` HR blob and `route.sampleTimestamps` array from the response; without it the payload is byte-identical to the v1.4.32 contract. Cross-user rows surface as 404 (existence channel sealed). parameters: - in: path name: id schema: type: string required: true - in: query name: compact schema: type: string enum: - "1" responses: "200": description: Workout detail. content: application/json: schema: $ref: "#/components/schemas/GetWorkoutResponse" "401": *a1 "403": *a5 "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 - badminton - 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 - APPLE_HEALTH externalId: type: string maxLength: 128 externalSourceVersion: type: string maxLength: 64 metadata: type: object propertyNames: type: string additionalProperties: {} route: type: object properties: geometry: type: object properties: type: type: string const: LineString coordinates: minItems: 2 maxItems: 20000 type: array items: type: array prefixItems: - type: number minimum: -180 maximum: 180 - type: number minimum: -90 maximum: 90 items: type: number required: - type - coordinates sampleTimestamps: type: array items: type: object properties: t: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ speedMs: type: number minimum: 0 maximum: 50 hr: type: integer minimum: 20 maximum: 300 required: - t required: - geometry 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/intake/bulk: post: tags: - Medications summary: Bulk medication-intake backfill (iOS SyncMode) description: Up to 500 intake entries per call, mirroring the mood-entries bulk envelope so the iOS sync engine reuses one retry/cursor path. Idempotent via the `Idempotency-Key` header plus per-entry `idempotencyKey` / `externalId`. Per-entry status (`inserted` / `updated` / `duplicate` / `skipped`) lets the client advance its cursor. Rate-limited 60/min/user. An `APPLE_HEALTH` entry must carry `externalId` and target a medication mirrored from Apple Health, else the whole batch 422s (`meta.errorCode = medications.intake.bulk.apple_health_not_mirrored`); an over-size batch 422s (`medications.intake.bulk.too_large`); a malformed body 422s (`medications.intake.bulk.invalid`). requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/BulkMedicationIntakeRequest" responses: "200": description: Batch processed (always 200 on a well-formed body). content: application/json: schema: $ref: "#/components/schemas/BulkMedicationIntakeResponseEnvelope" "401": *a1 "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`. v1.28 — a mirror create (`externalSource` + `externalId`) re-posting a pair the caller already holds returns the existing medication with 200 instead of a duplicate. v1.32.25 — a NEW mirror create is refused with 422 (`meta.errorCode = medications.mirror.limit_exceeded`) once the caller already holds 30 medications mirrored from the same external source; an idempotent re-post of a medication already mirrored is unaffected, and native creates are never gated. 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) plus the optimistic-concurrency `updatedAt` token. 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) plus its token. 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 plus the advanced `updatedAt` token is returned. Optimistic concurrency (v1.32.21): send `baseUpdatedAt` (the token from a prior read) and the write 409s if the stored blob changed since; omit it for the legacy unconditional write. Invalid bodies return the multi-issue 422 envelope." requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MedicationListLayoutPutBody" responses: "200": description: Presentation saved; the normalised blob plus the advanced token is echoed back. content: application/json: schema: $ref: "#/components/schemas/MedicationListLayoutSaved" "401": *a1 "409": description: Medications layout changed since it was loaded (optimistic-concurrency conflict). No write happened. Re-read the resource, re-apply the user's change against the fresh state, and resend with the new token. `meta.errorCode` = `medication_layout_conflict`. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": &a6 description: Request validation failed. When the body carried a `baseUpdatedAt` that could not be parsed as an ISO-8601 timestamp — including an explicit `null` — `meta.errorCode` = `invalid_base_updated_at` and nothing was written. Omit the key entirely for the unconditional write; do not send `null` for it. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "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. v1.19.0 (iOS#25) — also returns a server-computed `summary` (the canonical {`unitsRemaining`, `unitsTotal`, `dosesRemaining`, `dosesTotal`, `expiredUnits`}); clients render it directly rather than re-deriving the Bestand headline, so web and iOS agree. 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}/efficacy: get: tags: - Medications summary: Efficacy view for a medication ("Wirkung") description: "Returns the resolved, strictly-descriptive efficacy DTO: the outcome metric(s)/lab(s) the medication's class targets, the target series with start/dose-change/pause markers, a before/after-start comparison, the cadence-aware adherence lane, an optional conservative level-shift note, the data-floor state, and the retarget options. Association-only — there is no verdict / score field. `eligible:false` (with `reason`) marks a one-shot or no-target medication whose tab is hidden." parameters: - in: path name: id schema: type: string required: true responses: "200": description: Efficacy view. content: application/json: schema: $ref: "#/components/schemas/GetMedicationEfficacyResponse" "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}/efficacy/target: put: tags: - Medications summary: Set or clear the efficacy-target override description: Persists the user's explicit efficacy target for a medication (the only thing the view stores; everything else is derived each read). Pin exactly one of `measurementType` / `biomarkerId`, or pass `clear:true` to revert to the derived target. parameters: - in: path name: id schema: type: string required: true requestBody: required: true content: application/json: schema: type: object properties: clear: type: boolean measurementType: 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 - ANS_CHARGE - CARDIO_LOAD - SLEEP_SCORE - BODY_TEMPERATURE_DEVIATION - RESILIENCE - PHQ9_SCORE - GAD7_SCORE - GRIP_STRENGTH - PAIN_NRS - WAIST_CIRCUMFERENCE - WAIST_TO_HEIGHT - WHO5_SCORE - SCI_SCORE biomarkerId: type: string minLength: 1 maxLength: 64 primary: type: boolean responses: "200": description: Override set or cleared. content: application/json: schema: $ref: "#/components/schemas/SetMedicationEfficacyTargetResponse" "401": *a1 "404": description: Medication or biomarker not found. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/medications/{id}/dose-history: get: tags: - Medications summary: Per-slot dose-history ledger (Verlauf tab) description: "Returns the full per-slot ledger for the medication over [from, to]: every expected slot with a status (taken on-time / taken late / skipped / missed / upcoming) plus every off-schedule intake tagged ad-hoc. Built from the SAME band minter + `reconstructDoseHistory` the compliance % consumes, so the history view and the rate can never disagree. v1.32.8 (iOS #64) surfaces `intake.source` so a client can label how each dose was recorded. Ownership-scoped; rate-limited 60/min/user; reads only non-deleted rows. Auth via cookie or Bearer." parameters: - in: path name: id schema: type: string required: true - in: query name: from schema: description: Window start (inclusive). Defaults to 90 days before `to`; clamped to the medication's `createdAt` and a 366-day span floor. type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: Window start (inclusive). Defaults to 90 days before `to`; clamped to the medication's `createdAt` and a 366-day span floor. - in: query name: to schema: description: Window end (inclusive). Defaults to now. Must be ≥ `from`. type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: Window end (inclusive). Defaults to now. Must be ≥ `from`. responses: "200": description: Dose-history ledger. content: application/json: schema: $ref: "#/components/schemas/GetDoseHistoryResponse" "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/medications/intake/dose-history-import: post: tags: - Medications summary: Import a dose history exported from another medication tracker description: "Reads a ten-column CSV export covering a whole regimen (multiple medications in one file), matching each row against the caller's existing medications by name — a medication is NEVER created from an import file. `text/csv` carries the actual dose history (`Date`/`Scheduled Date` in separate columns, mapped onto `takenAt`/`scheduledFor`). `application/json` is accepted at the content-type check but the documented JSON export shape always refuses at 422 (`json_carries_no_intake_time`): it has no field for when a dose was actually taken, and inferring one from the scheduled time would manufacture an on-time history the file never claimed. Body capped at 16 MB and 30 000 importable rows. `?dryRun=1` parses, matches, and returns the file verdict WITHOUT writing. A real submission queues the write onto the same background job the per-medication import uses (chunked, heartbeated, resumable) and returns 202 with a `statusUrl` to poll. Shares the 5/hour `import:` rate bucket with every other bulk import surface. Auth via cookie or Bearer." parameters: - name: dryRun in: query required: false schema: type: string enum: - "1" - "true" description: When `1` / `true`, parse + match + return the file verdict without queuing a write. requestBody: required: true content: text/csv: schema: type: string application/json: schema: type: string responses: "200": description: Dry-run verdict (nothing written). content: application/json: schema: $ref: "#/components/schemas/MedicationDoseHistoryImportDryRunEnvelope" "202": description: Import queued. content: application/json: schema: $ref: "#/components/schemas/MedicationDoseHistoryImportQueuedEnvelope" "401": *a1 "409": description: A dose-history import is already in progress. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "413": description: File exceeds the 16 MB limit. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "415": description: Content-Type must be text/csv or application/json. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": description: "The file could not be read (see `MedicationDoseHistoryImportFatalReason` for the closed `errorCode` set), or it holds more than 30 000 importable rows (`errorCode: too_many_rows`)." content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "429": *a4 "503": description: Background worker is not available. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" /api/medications/{id}/intake/import/{jobId}/status: get: tags: - Medications summary: Poll a per-medication intake-import job description: Returns the queued/running/finished state of a per-medication intake-import job, scoped to the caller's own medication. A job created by the account-wide dose-history import (which carries no `medicationId`) never resolves through this path — poll its own `statusUrl` instead. v1.33.0 widened `result.skipReasons[].reason` / `progress.skipDetails[].reason` to the full 16-value `MedicationImportSkipReason` set. A foreign or unknown job/medication id is sealed as 404. Auth via cookie or Bearer. parameters: - in: path name: id schema: type: string required: true - in: path name: jobId schema: type: string required: true responses: "200": description: Current job status. content: application/json: schema: $ref: "#/components/schemas/MedicationIntakeImportJobStatusEnvelope" "401": *a1 "404": description: No such job for this medication (or owned by another user). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/medications/intake/dose-history-import/{jobId}/status: get: tags: - Medications summary: Poll an account-wide dose-history-import job description: "Returns the queued/running/finished state of a dose-history-import job created by `POST /api/medications/intake/dose-history-import` — the `statusUrl` that create response carries. Identical response projection to the per-medication `/api/medications/{id}/intake/import/{jobId}/status` (both read through the same `readMedicationIntakeImportJob` helper); this path narrows to the account-wide job (`medicationId: null`), so a per-medication job id 404s here and vice versa. A foreign or unknown job id is sealed as 404. Auth via cookie or Bearer." parameters: - in: path name: jobId schema: type: string required: true responses: "200": description: Current job status. content: application/json: schema: $ref: "#/components/schemas/MedicationIntakeImportJobStatusEnvelope" "401": *a1 "404": description: No such job for this account (or owned by another user). 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 OpenAI-compatible gateway's own base URL and model) and 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 plus the `updatedAt` token. 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. Supports optimistic concurrency: echo the `updatedAt` from the last read as `baseUpdatedAt` and the write is refused with 409 if the row changed since; omit it for the legacy unconditional write." requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PutCoachPrefsRequest" responses: "200": description: Saved preferences echoed back with the fresh token. content: application/json: schema: $ref: "#/components/schemas/PutCoachPrefsResponse" "401": *a1 "409": description: Coach preferences changed since it was loaded (optimistic-concurrency conflict). No write happened. Re-read the resource, re-apply the user's change against the fresh state, and resend with the new token. `meta.errorCode` = `coach_prefs_conflict`. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a6 "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/diabetes: get: tags: - Auth summary: Read per-user diabetes opt-in flag description: Returns the user's per-account diabetes opt-in flag. Default `false`. When `true`, the glucose target resolver applies the tighter ADA glycemic GOAL bands (fasting 80–130, postprandial < 180) instead of the general non-diabetic bands. Powers the Settings "I have diabetes / clinician glucose targets" toggle. Never inferred from a reading; asserts no diagnosis. responses: "200": description: Resolved flag. content: application/json: schema: $ref: "#/components/schemas/GetDiabetesResponse" "401": *a1 "422": *a3 "429": *a4 patch: tags: - Auth summary: Toggle per-user diabetes opt-in flag description: Sets the per-account diabetes opt-in flag. Idempotent — the DB write fires even when the value matches so the audit-log row mirrors the API call. Always returns the resolved next-state. `userId` is never accepted from the body. Rate-limit 60/min per user. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/DiabetesFlag" responses: "200": description: Resolved next-state echoed back for optimistic-update consumers. content: application/json: schema: $ref: "#/components/schemas/PatchDiabetesResponse" "401": *a1 "422": *a3 "429": *a4 /api/auth/me/health-score-config: get: tags: - Auth summary: Read which pillars count toward the Health Score description: "v1.35.0 — the resolved composition: which pillars count, which the person took out, whether they have chosen at all, and the recipe version. An account that never chose reads every pillar with `hasSelection: false` and `version: 0`, which is exactly what its number was already built from — there is no backfill because there is nothing to back-fill." responses: "200": description: The resolved composition. `updatedAt` is absent until a selection has been stored. content: application/json: schema: $ref: "#/components/schemas/HealthScoreConfigEnvelope" "401": *a1 "422": *a3 "429": *a4 patch: tags: - Auth summary: Choose which pillars count toward the Health Score description: "Replaces the recipe with the given positive selection and bumps the per-user recipe version by one. The write is refused when the selection cannot produce a score: at least one physical measurement, and at least three distinct areas of health. Blood pressure, glucose and cholesterol are three pillars but one area, which is the refusal that surprises people. Accepting the write also evicts the account's score caches, so the next read reflects the new composition rather than the previous hour's number. Optimistic concurrency: send `baseUpdatedAt` from a prior read and the write 409s if the recipe moved since; omit it for the unconditional write. Rate-limited to 60 writes per minute per user." requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PatchHealthScoreConfigRequest" responses: "200": description: The stored composition, echoed back with the incremented `version` and the advanced `updatedAt` token. content: application/json: schema: $ref: "#/components/schemas/PatchHealthScoreConfigEnvelope" "401": *a1 "409": description: Score selection changed since it was loaded (optimistic-concurrency conflict). No write happened. Re-read the resource, re-apply the user's change against the fresh state, and resend with the new token. `meta.errorCode` = `health_score_config_conflict`. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": description: "The write was refused and nothing was stored. `meta.errorCode` distinguishes the cases: `health_score_config.too_narrow` — the selection cannot produce a score (`meta.reason` is `measured_physiological_domain_required` or `three_domains_required`, and the `error` string is the sentence to show the person); `health_score_config.invalid` — the body failed validation, with the per-issue list under `details.issues`; `invalid_base_updated_at` — `baseUpdatedAt` was present but unparseable, `null` included, which is rejected rather than falling back to the unconditional write. A body that is not JSON at all returns the bare message `health-score-config.body.invalid_json` with no errorCode." content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "429": *a4 /api/auth/me/modules: get: tags: - Auth summary: Read the resolved per-user module enable/disable map description: "Returns the fully-resolved `{ : boolean }` map for every toggleable module. `cycle` reflects the cycle gate, `coach` reflects the resolved per-user opt-out + operator master flag, and the rest read the per-user disabled-allowlist (`modulePreferencesJson`). Default-on: a fresh account reads every module enabled. iOS/web read this to hide a whole module surface end-to-end." responses: "200": description: Resolved module map. content: application/json: schema: $ref: "#/components/schemas/GetModulesResponse" "401": *a1 "422": *a3 "429": *a4 patch: tags: - Auth summary: Update per-user module enable/disable preferences description: Merges the supplied keys into the persisted disabled-allowlist (`modulePreferencesJson`) field-by-field — a PATCH touching only one module leaves the siblings intact. Refuses to disable a core module (`weight`, `bloodPressure`, `pulse`, `medications`) or any unknown key with a 422. `userId` is never accepted from the body. Always returns the fully-resolved next-state map so clients can hard-set their optimistic update. Rate-limit 60/min per user. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ModulePrefsPatchRequest" responses: "200": description: Resolved next-state module map echoed back with the fresh `updatedAt` token. content: application/json: schema: $ref: "#/components/schemas/PatchModulesResponse" "401": *a1 "409": description: Module preferences changed since it was loaded (optimistic-concurrency conflict). No write happened. Re-read the resource, re-apply the user's change against the fresh state, and resend with the new token. `meta.errorCode` = `modules_conflict`. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a6 "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 - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA activeEnergy: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA walkingRunningDistance: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA flightsClimbed: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA sleep: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA weight: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA bloodPressure: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA pulse: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA bodyFat: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA bodyTemperature: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA spo2: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA hrv: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA restingHeartRate: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA vo2Max: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA skinTemperature: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA respiratoryRate: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA recovery: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA stress: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA sleepDebt: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA metricPriority: type: object properties: steps: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA activeEnergy: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA walkingRunningDistance: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA flightsClimbed: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA sleep: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA weight: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA bloodPressure: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA pulse: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA bodyFat: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA bodyTemperature: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA spo2: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA hrv: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA restingHeartRate: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA vo2Max: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA skinTemperature: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA respiratoryRate: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA recovery: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA stress: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA sleepDebt: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA 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 plus the `updatedAt` token. 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 with the fresh `updatedAt` token. content: application/json: schema: $ref: "#/components/schemas/PatchNotificationPrefsResponse" "401": *a1 "409": description: Notification preferences changed since it was loaded (optimistic-concurrency conflict). No write happened. Re-read the resource, re-apply the user's change against the fresh state, and resend with the new token. `meta.errorCode` = `notification_prefs_conflict`. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a6 "429": *a4 /api/auth/me/report-selection: get: tags: - Auth summary: Read the owner's saved doctor-report selection description: "Returns the saved report-scope profile, or `profile: null` when the account has never saved one (or the stored blob no longer parses against the current leaf catalogue). Replaces the retired `GET /api/auth/me/doctor-report-prefs`. Auth via cookie or Bearer." responses: "200": description: The saved profile, or null. content: application/json: schema: $ref: "#/components/schemas/GetReportSelectionResponseEnvelope" "401": *a1 "422": *a3 "429": *a4 put: tags: - Auth summary: Replace the owner's saved doctor-report selection description: Replace, not merge — the body states the whole scope or it is not a scope; a leaf omitted from the body is a leaf removed from the saved profile. An unknown leaf id is refused with a 422 that names it. The saved selection is also what the FHIR REST face and the MCP doctor-visit surfaces replay for a caller that cannot ask a human, so every write is audit-logged. Auth via cookie or Bearer. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SavedReportProfile" responses: "200": description: The saved profile, echoed back. content: application/json: schema: $ref: "#/components/schemas/PutReportSelectionResponseEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/insights/chat: get: tags: - Insights summary: List the caller's Coach conversations description: v1.18.0 — cursor-paginated list of the caller's Coach conversations for the history rail, most-recent activity first. Metadata only (id, title, timestamps, message count); message bodies are not decrypted here. `limit` defaults to 20, capped at 50; pass the returned `nextCursor` back as `cursor` for the next page (null at the end). v1.30.2 — optional `q` narrows the page to conversations whose TITLE contains the text (case-insensitive substring, capped at 200 chars); message bodies are encrypted at rest and are not searched. Coach-gated (`requireAssistantSurface("coach")`); a disabled surface 403s. Auth via cookie or Bearer; the owner is narrowed from the session. parameters: - name: cursor in: query required: false schema: type: string description: Id of the last conversation on the previous page. Omit for the first page. - name: limit in: query required: false schema: type: integer minimum: 1 maximum: 50 default: 20 description: Page size. Defaults to 20, capped at 50. - name: q in: query required: false schema: type: string maxLength: 200 description: v1.30.2 — case-insensitive substring filter over the conversation TITLE only (message bodies are encrypted and not searched). Omit for the unfiltered list. responses: "200": description: A page of the caller's conversations. content: application/json: schema: $ref: "#/components/schemas/CoachConversationsPageResponse" "401": *a1 "422": *a3 "429": *a4 post: tags: - Insights summary: Send a Coach turn (streaming reply) description: "v1.18.0 — sends a user turn and streams the assistant reply as Server-Sent Events. The response is `text/event-stream`, not JSON: one `data: \\n\\n` frame per event. Frame `type` is one of `token` (a chunk of reply text: `{ type, token }`), `provenance` (the evidence envelope: `{ type, metricSource }`), `suggestion` (a cadence-suggestion card: `{ type, suggestion }`), `reasoning` (v1.18.9, optional reasoning-summary text: `{ type, text }` — emitted only by reasoning-capable providers; absent otherwise), `done` (`{ type, conversationId, messageId, usage? }` — v1.18.9 adds the optional `usage` envelope `{ totalTokens, promptTokens?, completionTokens?, model? }`, server-authoritative; clients display it, never recompute), or `error` (`{ type, code, message }`). The HTTP status is 200 even for a provider/refusal outcome — clients dispatch on the `error` frame, not the status. Clients ignore unknown frame types (additive evolution). Omitting `conversationId` starts a new conversation. Coach-gated; budget- and rate-limited. Auth via cookie or Bearer." requestBody: required: true content: application/json: schema: type: object properties: conversationId: type: string minLength: 1 maxLength: 64 message: type: string minLength: 1 maxLength: 4000 prefill: type: string maxLength: 2000 locale: type: string enum: - en - de scope: type: object properties: sources: maxItems: 40 type: array items: type: string enum: - bp - weight - pulse - mood - compliance - hrv - sleep - resting_hr - steps - active_energy - flights - distance - vo2_max - body_temp - walking_hr - respiratory_rate - spo2 - pulse_wave_velocity - vascular_age - body_fat - fat_mass - fat_free_mass - muscle_mass - lean_body_mass - bone_mass - total_body_water - bmi - visceral_fat - glucose - walking_steadiness - walking_asymmetry - walking_double_support - walking_step_length - walking_speed - audio_env - audio_headphone - audio_event - daylight - skin_temp - workouts window: type: string enum: - last7days - last30days - last90days - lastYear - allTime guidedQuestion: type: string minLength: 1 maxLength: 500 workoutId: type: string maxLength: 64 required: - message responses: "200": description: Server-Sent Events stream of `token` / `provenance` / `done` / `error` frames. content: text/event-stream: schema: type: string description: "SSE frames: `data: \\n\\n`. See the operation description for the per-`type` frame shapes." "401": *a1 "403": description: "Coach surface disabled (`errorCode: assistant.disabled.coach`) or AI consent required (`errorCode: consent.ai.required`)." content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "413": description: Request body exceeds the 64 KB cap. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/insights/chat/{id}: get: tags: - Insights summary: Read one Coach conversation with all messages description: v1.18.0 — returns one conversation with every message decrypted server-side and ordered oldest-first, plus the rolling `summary` when one is on file. The client renders the bodies directly; no decryption key is involved. A foreign / unknown id maps to 404 (never 403) so the existence channel does not leak across accounts. Coach-gated. Auth via cookie or Bearer. parameters: - name: id in: path required: true schema: type: string description: Conversation id. responses: "200": description: The conversation with all decrypted messages. content: application/json: schema: $ref: "#/components/schemas/CoachConversationDetailResponse" "401": *a1 "404": description: Conversation not found or not owned by the caller. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 delete: tags: - Insights summary: Delete one Coach conversation description: v1.18.0 — hard-deletes a conversation and every message under it. A foreign / unknown id maps to 404 (never 403). Coach-gated. Auth via cookie or Bearer. parameters: - name: id in: path required: true schema: type: string description: Conversation id. responses: "200": description: The conversation was deleted. content: application/json: schema: $ref: "#/components/schemas/CoachConversationDeleted" "401": *a1 "404": description: Conversation not found or not owned by the caller. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/insights/chat/fenced: post: tags: - Insights summary: Send a FENCED multi-document coach turn (streaming reply) description: "v1.29.x (S7) — sends a user turn and streams a grounded prose reply about the documents attached to a coach conversation, as Server-Sent Events (`text/event-stream`: one `data: \\n\\n` frame per event; `token` / `done` / `error`, HTTP 200 even for a provider/refusal outcome). FENCED by construction: NO tools, NO health snapshot; every attached document is fenced as untrusted DATA (per-document header fields marker-scrubbed); the single picked provider is egress-consent-checked once per attached document (403 `consent.ai.required` if any fails); the reply is numerically grounded against the LIVE attachments' figures only. `conversationId` continues an existing fenced thread; `attachmentIds` (first-turn ONLY, min 1, max 5) creates a fresh fenced thread — supplying BOTH is a 422. The body is `.strict()`: `scope` / `guidedQuestion` / `prefill` / `userId` are rejected. A plain tool conversation 404s here. Module-gated on `inboundDocuments`; rate-limited (shared `document-chat` bucket). Renders as plain text (no markdown). Auth via cookie or Bearer." requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/FencedCoachChatRequest" responses: "200": description: Server-Sent Events stream of `token` / `done` / `error` frames. content: text/event-stream: schema: type: string description: "SSE frames: `data: \\n\\n`. See the operation description for the per-`type` frame shapes." "401": *a1 "403": description: "AI consent required for an external provider (`errorCode: consent.ai.required`). A 422 (`stdResponses`) covers an un-indexed / unavailable attachment (`coach.fenced.attachmentUnavailable`), the attachment cap (`coach.fenced.attachmentLimit`), or `attachmentIds` sent with a `conversationId` (`coach.fenced.attachmentConflict`)." content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/insights/chat/{id}/attachments: post: tags: - Insights summary: Attach a document to a coach conversation description: "v1.29.x (S7) — attaches one already-stored, content-indexed document to an existing conversation and sets its sticky `documentScoped` flag TRUE (the one legal, privilege-reducing tool→fenced transition; audit-logged when a flip occurs). Validates the document is owned + live + indexed + within the 5-document cap. Idempotent: attaching an already-attached document is a 200. A foreign / unknown conversation or document maps to 404. Module-gated on `inboundDocuments`; rate-limited. Auth via cookie or Bearer." parameters: - name: id in: path required: true schema: type: string description: Conversation id. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CoachAttachmentCreateRequest" responses: "200": description: The refreshed live attachment set + the fenced flag. content: application/json: schema: $ref: "#/components/schemas/CoachAttachmentsResponse" "401": *a1 "404": description: Conversation or document not found / not owned. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/insights/chat/{id}/attachments/{documentId}: delete: tags: - Insights summary: Detach a document from a coach conversation description: v1.29.x (S7) — removes the document from the conversation. The conversation stays FENCED (the sticky flag is never cleared — a thread whose history may contain document-derived text must never regain the tool loop). Detaching the last document leaves a fenced conversation with zero attachments. A missing row / foreign conversation maps to 404. Module-gated on `inboundDocuments`; rate-limited. Auth via cookie or Bearer. parameters: - name: id in: path required: true schema: type: string description: Conversation id. - name: documentId in: path required: true schema: type: string description: The attached document's id. responses: "200": description: The refreshed live attachment set + the fenced flag. content: application/json: schema: $ref: "#/components/schemas/CoachAttachmentsResponseDetach" "401": *a1 "404": description: Attachment or conversation not found / not owned. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "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) plus the optimistic-concurrency `updatedAt` token. 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) plus its token. 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 plus the advanced `updatedAt` token is returned. Optimistic concurrency (v1.32.21): send `baseUpdatedAt` (the token from a prior read) and the write 409s if the stored row changed since; omit it for the legacy unconditional write. Invalid bodies return the multi-issue 422 envelope, matching the dashboard-widgets route." requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/InsightsLayoutPutBody" responses: "200": description: Layout saved; the normalised layout plus the advanced token is echoed back. content: application/json: schema: $ref: "#/components/schemas/InsightsLayoutSaved" "401": *a1 "409": description: Insights layout changed since it was loaded (optimistic-concurrency conflict). No write happened. Re-read the resource, re-apply the user's change against the fresh state, and resend with the new token. `meta.errorCode` = `insights_layout_conflict`. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a6 "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/insights/coach/nudge-status: get: tags: - Insights summary: Whether an unopened Coach message is waiting description: v1.18.6 (CCH-03) — server-authoritative unread signal for the Coach FAB. `unread` is true when the caller's newest Coach ASSISTANT message (a proactive nudge or any reply) is newer than `User.coachLastSeenAt`; a user who has never opened the Coach reads an existing nudge as unread exactly once. `nudgedAt` carries that newest assistant-message timestamp (null when none exists) so the client can key a local seen-mirror on a stable value. Coach-gated (`requireAssistantSurface("coach")`); a disabled surface 403s. Auth via cookie or Bearer; the owner is narrowed from the session. responses: "200": description: The current unread state. content: application/json: schema: $ref: "#/components/schemas/CoachNudgeStatus" "401": *a1 "422": *a3 "429": *a4 /api/insights/coach/seen: post: tags: - Insights summary: Mark the Coach as opened (clear the unread dot) description: v1.18.6 (CCH-03) — opening the Coach (drawer or full page) stamps `User.coachLastSeenAt = now()`, so `GET /api/insights/coach/nudge-status` then reports no assistant message newer than the stamp and the FAB drops the unread dot. Server-authoritative, so the cleared state follows the caller across web + iOS rather than just the opening device. No request body — the timestamp is server-minted, so a client can never backdate the stamp to suppress a future nudge. Coach-gated; a disabled surface 403s. Auth via cookie or Bearer. responses: "200": description: The Coach was marked opened; the stamp is echoed back. content: application/json: schema: $ref: "#/components/schemas/CoachSeenResponse" "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. Optimistic concurrency (v1.32.21): send `baseUpdatedAt` (the `updatedAt` from a prior read) and the write 409s if the stored self-context changed since; omit it for the legacy unconditional write. The token is per-surface (`UserHealthProfile.updatedAt`), so it is not perturbed by unrelated account writes." 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 aiIncludedSections: maxItems: 8 type: array items: type: string enum: - ABOUT_ME - CONDITIONS - ALLERGIES - COACH_FOCUS - FAMILY_HISTORY - SMOKING_STATUS - ALCOHOL_PATTERN - SHIFT_SCHEDULE baseUpdatedAt: description: "Optimistic-concurrency base token: the `updatedAt` the client last read for this resource. Omit it for the legacy unconditional write (older clients are unaffected). When present, the write is guarded on the stored row still carrying this exact value — a stale token fails with 409 and changes nothing. A present-but-unparseable value fails with 422 and `meta.errorCode` = `invalid_base_updated_at` — note that this is NOT the unconditional write: sending `null` or a malformed string is rejected rather than treated as an omitted token. Treat as opaque: only ever echo a server-returned value, never parse or synthesise it." type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: The effective (trimmed) state echoed back plus the freshly derived pending questions and the advanced `updatedAt` token. content: application/json: schema: $ref: "#/components/schemas/PutCoachAboutMeResponse" "401": *a1 "409": description: Self-context changed since it was loaded (optimistic-concurrency conflict). No write happened. Re-read the resource, re-apply the user's change against the fresh state, and resend with the new token. `meta.errorCode` = `about_me_conflict`. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a6 "429": *a4 /api/anamnesis/facts: get: tags: - Insights summary: Read effective-dated anamnesis facts description: "Returns the current value and immutable revision history for the caller's smoking status, alcohol pattern and shift schedule. Values are decrypted server-side; unreadable ciphertext returns `value: null` with `unreadable: true`." responses: "200": description: Current facts and their revision history. content: application/json: schema: $ref: "#/components/schemas/GetAnamnesisFactsResponse" "401": *a1 "422": *a3 "429": *a4 post: tags: - Insights summary: Create an anamnesis fact description: Creates the first current revision for one closed fact kind. The answer is encrypted before persistence. A second current value for the same kind returns 409; corrections use the revision endpoint. requestBody: required: true content: application/json: schema: oneOf: - type: object properties: kind: type: string const: SMOKING_STATUS value: type: string enum: - NEVER - FORMER - CURRENT required: - kind - value - type: object properties: kind: type: string const: ALCOHOL_PATTERN value: type: string enum: - NONE - OCCASIONAL - WEEKLY - MOST_DAYS required: - kind - value - type: object properties: kind: type: string const: SHIFT_SCHEDULE value: type: string enum: - NONE - FIXED_SHIFT - ROTATING required: - kind - value type: object responses: "201": description: The newly created current revision. content: application/json: schema: $ref: "#/components/schemas/CreateAnamnesisFactResponse" "401": *a1 "409": description: A current revision already exists for this fact kind. `meta.errorCode` = `anamnesis.fact.currentExists`. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/anamnesis/facts/{id}: patch: tags: - Insights summary: Correct a current anamnesis fact description: Closes the current revision at the correction instant and creates a successor. Prior ciphertext and its half-open validity interval remain in history. parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: value: type: string minLength: 1 required: - value responses: "200": description: The successor revision. content: application/json: schema: $ref: "#/components/schemas/CorrectAnamnesisFactResponse" "401": *a1 "404": &a7 description: No current revision with this id exists for the authenticated account. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "409": description: Anamnesis fact changed since it was loaded (optimistic-concurrency conflict). No write happened. Re-read the resource, re-apply the user's change against the fresh state, and resend with the new token. `meta.errorCode` = `anamnesis.fact.conflict`. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 delete: tags: - Insights summary: Remove a current anamnesis fact description: Closes the caller-owned current revision without deleting its encrypted history. The fact kind then resolves as not recorded until a new value is created. The path revision id is the optimistic concurrency token. parameters: - name: id in: path required: true schema: type: string responses: "200": description: Metadata for the revision whose interval was closed. content: application/json: schema: $ref: "#/components/schemas/RemoveAnamnesisFactResponse" "401": *a1 "404": *a7 "409": description: Anamnesis fact changed since it was loaded (optimistic-concurrency conflict). No write happened. Re-read the resource, re-apply the user's change against the fresh state, and resend with the new token. `meta.errorCode` = `anamnesis.fact.conflict`. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "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/coach/plans: get: tags: - Insights summary: List the caller's Coach goal / if-then plans description: v1.21.3 (B1) — returns the durable plans the Coach has proposed for the caller, newest first, each decrypted on the fly. A plan is an "if-then" implementation intention tied to one metric, with an optional target. The Coach extractor writes a plan as `proposed`; only `PATCH /api/coach/plans/{id}` activates it. Pass `?status=` to filter to one lifecycle status, or `?scope=` for a named group (open = proposed + active + review_due, past = met + abandoned + reviewed, all = every non-deleted plan) — mutually exclusive. Both omitted returns the non-terminal set (proposed + active). Coach-gated (`requireModuleEnabled("coach")`); a disabled surface 403s. 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. parameters: - name: status in: query required: false schema: type: string enum: - proposed - active - met - abandoned - review_due - reviewed description: Filter to a single lifecycle status. Mutually exclusive with `scope`. Omit both for the non-terminal set (proposed + active). - name: scope in: query required: false schema: type: string enum: - open - past - all description: "Named status group: open (proposed + active + review_due), past (met + abandoned + reviewed), or all. Mutually exclusive with `status`." responses: "200": description: The caller's plans. content: application/json: schema: $ref: "#/components/schemas/CoachPlansList" "401": *a1 "403": description: Coach surface disabled. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/coach/plans/{id}: patch: tags: - Insights summary: Confirm or update a Coach plan's lifecycle description: v1.21.3 (B1) — confirm a proposed plan (status proposed → active) or mark it met / abandoned, and optionally set / clear a review date. The body carries ONLY lifecycle fields — never the metric or the encrypted free text — so a client can change a plan's status but never inject or overwrite its prose. A foreign / unknown / already-deleted id maps to 404 (never 403) so the existence channel does not leak across accounts. Coach-gated. Auth via cookie or Bearer; the owner is narrowed from the session. parameters: - name: id in: path required: true schema: type: string description: Plan id. requestBody: required: true content: application/json: schema: type: object properties: status: type: string enum: - proposed - active - met - abandoned - review_due - reviewed reviewDate: 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" additionalProperties: false responses: "200": description: The plan after the lifecycle update. content: application/json: schema: $ref: "#/components/schemas/CoachPlanUpdated" "401": *a1 "403": description: Coach surface disabled. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "404": description: Plan not found or not owned by the caller. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 delete: tags: - Insights summary: Soft-delete one Coach plan description: "v1.21.3 (B1) — soft-deletes a single plan 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." parameters: - name: id in: path required: true schema: type: string description: Plan id. responses: "200": description: "The plan was soft-deleted (`deleted: true`) or the id matched nothing the caller owns (`deleted: false`)." content: application/json: schema: $ref: "#/components/schemas/CoachPlanDeleted" "401": *a1 "403": description: Coach surface disabled. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/coach/reminder-suggestions: post: tags: - Insights summary: Act on a Coach cadence suggestion description: 'v1.18.1 — accept / dismiss / stop a Coach-proposed measurement cadence. Coach-gated (`requireModuleEnabled("coach")`); a disabled surface 403s. `accept` resolves the cadence from the closed server-side catalog and creates a `MeasurementReminder` with `origin: COACH` (201), or returns 200 with `duplicate: true` when a live COACH reminder for that metric already exists — idempotent against a re-tapped or stale card, with a partial unique index as the structural backstop. Per-user rate-limited (429 on excess). Auth via cookie or Bearer; the owner is narrowed from the session.' requestBody: required: true content: application/json: schema: type: object properties: cadenceId: type: string minLength: 1 maxLength: 64 action: type: string enum: - accept - dismiss - stop required: - cadenceId - action responses: "200": description: "A dismiss/stop, or an accept that matched an existing reminder (`duplicate: true`)." content: application/json: schema: $ref: "#/components/schemas/CoachReminderSuggestionResultOk" "201": description: An accept that created a new reminder. content: application/json: schema: $ref: "#/components/schemas/CoachReminderSuggestionResultCreated" "401": *a1 "403": description: Coach surface (or the cadence's module) disabled. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/coach/reminders: get: tags: - Insights summary: List the caller's Coach reminders description: v1.22 (B2/B6) — the durable "remind me about X" memory the Coach captured inline, decrypted on the fly, soonest-due first. Pass `?status=` (one status or a comma set like `due,surfaced` for the in-app tile); omitted returns the non-terminal set (proposed + active + due + surfaced). Coach-gated (`requireModuleEnabled("coach")`). Auth via cookie or Bearer; the owner is narrowed from the session. Undecryptable rows are omitted. parameters: - name: status in: query required: false schema: type: string description: Filter to one status or a comma-separated set (proposed | active | due | surfaced | done | dismissed). Omit for the non-terminal set. responses: "200": description: The caller's reminders. content: application/json: schema: $ref: "#/components/schemas/CoachRemindersList" "401": *a1 "403": description: Coach surface disabled. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 post: tags: - Insights summary: Create a Coach reminder manually description: v1.22 (B6) — create a reminder from the ledger. `note` is the only writable content; `when` is the closed grammar (ISO date | +Nd / +Nw | a context cue) resolved into the trigger server-side; `metric` optional. The per-user cap returns 409 when the non-terminal set is full. Coach-gated. Auth via cookie or Bearer; the owner is narrowed from the session. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CoachReminderCreate" responses: "201": description: The created reminder. content: application/json: schema: $ref: "#/components/schemas/CoachReminderCreated" "401": *a1 "403": description: Coach surface disabled. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "409": description: The per-user reminder cap is full. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/coach/reminders/{id}: patch: tags: - Insights summary: Confirm or update a Coach reminder's lifecycle description: v1.22 (B6) — confirm a proposed reminder (→ active), mark it done / dismissed, or re-schedule via the closed `when` grammar (null clears the due moment). The body never carries the note text. A foreign / unknown / already-deleted id maps to 404 (never 403) so the existence channel does not leak. Coach-gated. Auth via cookie or Bearer. parameters: - name: id in: path required: true schema: type: string description: Reminder id. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CoachReminderPatch" responses: "200": description: The reminder after the lifecycle update. content: application/json: schema: $ref: "#/components/schemas/CoachReminderUpdated" "401": *a1 "403": description: Coach surface disabled. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "404": description: Reminder not found or not owned by the caller. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 delete: tags: - Insights summary: Soft-delete one Coach reminder description: "v1.22 (B6) — soft-deletes a single reminder owned by the caller. An unknown / cross-user / already-deleted id is an idempotent no-op returning `{ deleted: false }`. Coach-gated. Auth via cookie or Bearer." parameters: - name: id in: path required: true schema: type: string description: Reminder id. responses: "200": description: "The reminder was soft-deleted (`deleted: true`) or the id matched nothing the caller owns (`deleted: false`)." content: application/json: schema: $ref: "#/components/schemas/CoachReminderDeleted" "401": *a1 "403": description: Coach surface disabled. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/coach/suggested-actions: post: tags: - Insights summary: Confirm a Coach action card description: v1.22 (F6) — the confirm half of the generalised propose→confirm moat. `actionType` is from the CLOSED allowlist (checkup.create | reminder.note) — NEVER a medication or clinical change. `checkup.create` builds a preventive-care Vorsorge `MeasurementReminder` (free-text label + a closed interval id resolved to an RRULE server-side); `reminder.note` builds a `CoachReminder`. Nothing is created without this explicit confirm. Coach-gated (`requireModuleEnabled("coach")`); per-user rate-limited (429). Auth via cookie or Bearer; the owner is narrowed from the session, built field-by-field — no mass assignment, no IDOR. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CoachSuggestedAction" responses: "201": description: The action was applied (entity created server-side). content: application/json: schema: $ref: "#/components/schemas/CoachSuggestedActionResultCreated" "401": *a1 "403": description: Coach surface disabled. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "409": description: The per-user reminder cap is full (reminder.note). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "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/cards: get: tags: - Insights summary: Insight cards (iOS adapter) description: "v1.4.31 — the native-client adapter over the same alert rule engine the web comprehensive surface consumes: measurements, BP-in-target, weight trend, pulse, and cadence-aware medication compliance are fed through `generateAlerts()` and each resulting `HealthAlert` is re-shaped to the iOS Insight card model. Deterministic — no LLM call on this path. Module-gated on `insights` and the operator `insightStatus` assistant surface. Auth via cookie or Bearer." responses: "200": description: The list of insight cards (possibly empty). content: application/json: schema: $ref: "#/components/schemas/InsightsCardsEnvelope" "401": *a1 "403": *a5 "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 - ANS_CHARGE - CARDIO_LOAD - SLEEP_SCORE - BODY_TEMPERATURE_DEVIATION - RESILIENCE - PHQ9_SCORE - GAD7_SCORE - GRIP_STRENGTH - PAIN_NRS - WAIST_CIRCUMFERENCE - WAIST_TO_HEIGHT - WHO5_SCORE - SCI_SCORE 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 - SLEEP_SCORE - ANS_CHARGE - DAY_STRAIN - WORKOUT_STRAIN - CARDIO_LOAD - AVERAGE_HEART_RATE - MAX_HEART_RATE - ENERGY_EXPENDITURE_KJ - GRIP_STRENGTH - PAIN_NRS - WAIST_CIRCUMFERENCE - WAIST_TO_HEIGHT 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/biomarker-assessment: get: tags: - Insights summary: Per-biomarker assessment description: "Data-driven plain-language assessment for one user-scoped lab biomarker, reading its `LabResult` history. Identical envelope to the metric-status card so the `InsightStatusCard` consumes it unchanged. Read-only: a cache miss warms a generation out of band and serves the last-good text meanwhile (stale-while-revalidate); the assessment regenerates only when a new reading lands. A marker with no numeric readings returns `insufficient` without an LLM call. Auth via cookie or Bearer." parameters: - in: query name: biomarkerId schema: type: string minLength: 1 maxLength: 64 description: User-scoped biomarker id to assess. A cross-user or unknown id returns an `insufficient` envelope, not a 404 (existence sealed). required: true description: User-scoped biomarker id to assess. A cross-user or unknown id returns an `insufficient` envelope, not a 404 (existence sealed). - 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/BiomarkerAssessmentResponseEnvelope" "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 - HEALTH_SCORE - SAME_TIME_BASELINE 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: The single measurement type a baseline metric works over. `VITALS_BASELINE` takes one of the eleven vitals (`RESTING_HEART_RATE` through `WEIGHT`; defaults to `RESTING_HEART_RATE`). `SAME_TIME_BASELINE` takes one of the four cumulative day metrics — `ACTIVITY_STEPS`, `ACTIVE_ENERGY_BURNED`, `WALKING_RUNNING_DISTANCE`, `FLIGHTS_CLIMBED`. Ignored by the composite metrics. A type the named metric does not support yields an `insufficient` value rather than a 422, so client 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 - ACTIVITY_STEPS - ACTIVE_ENERGY_BURNED - WALKING_RUNNING_DISTANCE - FLIGHTS_CLIMBED description: The single measurement type a baseline metric works over. `VITALS_BASELINE` takes one of the eleven vitals (`RESTING_HEART_RATE` through `WEIGHT`; defaults to `RESTING_HEART_RATE`). `SAME_TIME_BASELINE` takes one of the four cumulative day metrics — `ACTIVITY_STEPS`, `ACTIVE_ENERGY_BURNED`, `WALKING_RUNNING_DISTANCE`, `FLIGHTS_CLIMBED`. Ignored by the composite metrics. A type the named metric does not support yields an `insufficient` value rather than a 422, so client 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 the single measurement type a baseline metric works over — a vital for `VITALS_BASELINE`, a cumulative day metric for `SAME_TIME_BASELINE`; see the `type` parameter on `GET /api/insights/derived`). 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 the single measurement type a baseline metric works over — a vital for `VITALS_BASELINE`, a cumulative day metric for `SAME_TIME_BASELINE`; see the `type` parameter on `GET /api/insights/derived`). 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/glp1-plateau: get: tags: - Insights summary: GLP-1 weight-plateau detection description: "Deterministic (non-LLM) weight-plateau read for users on an active GLP-1 medication: flags a stable dose held for at least the trailing window with no weight loss beyond the threshold. `plateau` is null whenever the condition does not hold, so clients hide the note cleanly. Association only — no verdict, no dose advice. Auth via cookie or Bearer." responses: "200": description: Plateau context (or null) plus the window length. content: application/json: schema: $ref: "#/components/schemas/InsightsGlp1PlateauResponseEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/insights/ecg: get: tags: - Insights summary: ECG recording list (metadata only) description: "v1.28.50 — the authenticated user's ECG recordings as a cheap, index-covered metadata list (recorded time, duration, sampling rate, sample count, average heart rate, lead, and the DEVICE's own rhythm classification). NEVER decrypts or returns the waveform — the per-recording strip is fetched on demand from GET /api/insights/ecg/{id}. Reflects only the recording device's certified on-device classification, verbatim; HealthLog never re-classifies an ECG or produces a diagnosis. Data-availability-gated: an empty account returns `hasRecordings: false`. Module-gated on `insights` and the operator `insightStatus` assistant surface; no LLM call. Auth via cookie or Bearer." responses: "200": description: The ECG recording list (possibly empty). content: application/json: schema: $ref: "#/components/schemas/EcgListResponseEnvelope" "401": *a1 "403": *a5 "422": *a3 "429": *a4 post: tags: - Insights summary: Ingest one ECG recording description: "The live ECG ingest: one Apple Watch recording per request, for a client draining its HealthKit ECG observer. Before this existed, a watch ECG could only reach HealthLog inside a full `export.zip`. One recording per request because a 30 s / 512 Hz strip is ~15 360 samples. Samples are INTEGER MICRO-VOLTS (convert from HealthKit's Volts), stored AES-256-GCM encrypted; `sampleCount` and `durationSeconds` are derived server-side. The `classification` is the device's own verdict stored verbatim — HealthLog never reads the waveform to produce or revise one. `source` accepts `APPLE_HEALTH` only; `userId` comes from the session and is never a body field. Unknown body keys are rejected with a 422 naming them. No `Idempotency-Key` is needed: the recording carries its own identity, so a retry resolves to the same row by construction — see `status` for what a re-post reports. Limits: 32 768 samples, 2 MB body, 60 recordings per minute per user. Module-gated on `insights` and the operator `insightStatus` assistant surface; no LLM call. Auth via cookie or Bearer." requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/EcgIngestRequest" responses: "200": description: "The recording was already stored: `updated` (same id, overwritten in place) or `duplicate` (already present under a different id, nothing written)." content: application/json: schema: $ref: "#/components/schemas/EcgIngestResponseEnvelope" "201": description: "A new recording was stored (`status: inserted`)." content: application/json: schema: $ref: "#/components/schemas/EcgIngestCreatedEnvelope" "401": *a1 "403": *a5 "422": *a3 "429": *a4 /api/insights/ecg/{id}: get: tags: - Insights summary: One ECG recording with waveform description: v1.28.50 — one recording's decrypted waveform plus metadata and the DEVICE's verbatim classification. Ownership is narrowed in the query where (`{ id, userId }`) so a cross-user read is structurally impossible; a foreign or unknown id 404s (existence sealed). The waveform is AES-256-GCM at rest, decrypted through the fail-closed codec. By default the ~9000-sample strip is min/max-decimated to ~2500 display points so R-wave peaks survive; `?full=1` returns the raw array. HealthLog does not interpret the trace, measure intervals, annotate beats, or emit a verdict of its own. Module-gated on `insights` and the operator `insightStatus` assistant surface; no LLM call. `no-store`. Auth via cookie or Bearer. parameters: - in: path name: id schema: type: string description: The ECG recording id (cuid). required: true description: The ECG recording id (cuid). - in: query name: full schema: description: "`1` returns the RAW, un-decimated sample array (the true-calibration 25 mm/s · 10 mm/mV zoom view). Omitted returns the min/max-decimated ~2500-point fit-to-width overview (`decimated: true`)." type: string const: "1" description: "`1` returns the RAW, un-decimated sample array (the true-calibration 25 mm/s · 10 mm/mV zoom view). Omitted returns the min/max-decimated ~2500-point fit-to-width overview (`decimated: true`)." responses: "200": description: The recording's waveform + metadata + device classification. content: application/json: schema: $ref: "#/components/schemas/EcgDetailResponseEnvelope" "401": *a1 "403": *a5 "404": description: No ECG recording with that id for the authenticated user (existence sealed — a foreign id is indistinguishable from a missing one). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/insights/rhythm-events: get: tags: - Insights summary: Device-flagged rhythm/HR/steadiness event timeline description: "v1.10.0 (WX-B) — the authenticated user's timeline of device-flagged EVENT rows: irregular-rhythm / high-HR / low-HR / walking-steadiness / breathing-disturbance notifications the user's wearable (Apple Watch / Withings ScanWatch) already produced and synced. AWARENESS / SCREENING of the DEVICE's own decision — HealthLog stores and reflects ONLY the classification result the device's certified on-device algorithm emitted, verbatim; it never re-classifies and never produces a HealthLog diagnosis. `classification` carries the full six-value verdict set (the three ECG verdicts plus the two walking-steadiness severities plus the neutral FIRED verdict) — a distinct, wider enum than the three-value one on GET /api/insights/ecg. Data-availability-gated: an account with no event rows returns `hasEvents: false`. Module-gated on `insights` and the operator `insightStatus` assistant surface; no LLM call. Auth via cookie or Bearer." responses: "200": description: The device-flagged event timeline (possibly empty). content: application/json: schema: $ref: "#/components/schemas/RhythmEventsResponseEnvelope" "401": *a1 "403": *a5 "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/insights/patterns: get: tags: - Insights summary: List current correlation patterns description: Returns the authenticated account's currently accepted persisted correlation identities, evidence, and dismissal timestamps. responses: "200": description: Current persisted correlation patterns. content: application/json: schema: $ref: "#/components/schemas/CorrelationPatternListResponseEnvelope" "401": *a1 "403": *a5 "422": *a3 "429": *a4 /api/insights/patterns/{id}: patch: tags: - Insights summary: Update correlation pattern dismissal description: Dismisses or restores one current account-owned pattern. A dismissal remains effective until the accepted evidence changes materially. parameters: - in: path name: id schema: type: string required: true requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateCorrelationPatternRequest" responses: "200": description: Updated dismissal decision. content: application/json: schema: $ref: "#/components/schemas/UpdateCorrelationPatternResponseEnvelope" "401": *a1 "403": *a5 "404": description: No current pattern with that id for this account. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/insights/health-status: get: tags: - Insights summary: Baseline-drift health status description: Surfaces vitals drifting from the user's personal normal — out-of-band deviations from the personal-baseline engine plus dated, sustained level shifts from the changepoint detector. Pure compute over the rollup tier; awareness only, never a diagnosis. Requires the insights module. Cookie or Bearer auth. responses: "200": description: The drift summary. content: application/json: schema: $ref: "#/components/schemas/InsightsHealthStatusEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/insights/breathing-screening: get: tags: - Insights summary: Sleep-breathing screening signal description: Summarises the last ~30 nights of the per-night sleep-breathing-disturbance index plus device-flagged events into a calm awareness read. A screening signal only — never a HealthLog diagnosis. Requires the insights module. Cookie or Bearer auth. responses: "200": description: The breathing-screening summary. content: application/json: schema: $ref: "#/components/schemas/InsightsBreathingScreeningEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/insights/labs-changes: get: tags: - Insights summary: What changed since your last lab panel description: Pairs the two most-recent numeric lab panels and reports each shared analyte's signed delta plus its reference-band standing. Absent when there are fewer than two panels or no analyte is shared. Neutral framing, never a diagnosis. Cookie or Bearer auth. responses: "200": description: The lab-change summary. content: application/json: schema: $ref: "#/components/schemas/InsightsLabsChangesEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/mood-entries/bulk: post: tags: - Mood summary: Bulk mood backfill (iOS SyncMode) description: "Drains the iOS local mood log in one shot: up to 500 entries per call with per-entry UPSERT semantics keyed by `externalId`, so an adopt-on-pair backfill or a retried batch is idempotent. Idempotent via the `Idempotency-Key` header too. Per-entry status (`inserted` / `duplicate` / `skipped`) advances the client cursor. Rate-limited 60/min/user. An over-size batch 422s (`meta.errorCode = mood.bulk.too_large`); a malformed body 422s (`mood.bulk.invalid`)." requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/BulkMoodRequest" responses: "200": description: Batch processed (always 200 on a well-formed body). content: application/json: schema: $ref: "#/components/schemas/BulkMoodResponseEnvelope" "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": &a8 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": *a8 "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": *a8 "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": *a8 "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": *a8 "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: $ref: "#/components/schemas/MoodTagLayoutPutRequest" responses: "200": description: Merged + resolved layout with the fresh token. content: application/json: schema: $ref: "#/components/schemas/MoodTagLayoutPutEnvelope" "401": *a1 "409": description: Mood-tag layout changed since it was loaded (optimistic-concurrency conflict). No write happened. Re-read the resource, re-apply the user's change against the fresh state, and resend with the new token. `meta.errorCode` = `mood_tag_layout_conflict`. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a6 "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 /api/import/csv: post: tags: - Import summary: Import measurements from CSV description: "Bulk-import measurements from a CSV file (web cold-start escape hatch). Body is raw `text/csv` (≤ 16 MB, ≤ 10 000 valid rows). Header (order-independent): `type,value,unit,measuredAt[,glucoseContext,notes,externalId]`. `measuredAt` must carry an explicit ISO-8601 offset and is bounded (no future beyond a 5-min skew, no instant before 1900). Glucose accepts `mmol/L` (converted to canonical `mg/dL`); weight accepts `lb` (converted to `kg`). `glucoseContext` is optional on a `BLOOD_GLUCOSE` row — a blank cell stores no context, which is the normal shape of a continuous-sensor export; a non-empty value is still checked against the enum, and the column is refused on any other type. An `externalId` column makes re-upload idempotent (upsert on `(userId, type, source=IMPORT, externalId)`); without it a re-upload duplicates. `?dryRun=1` validates + previews without writing. Shares the 5/hour `import:` rate bucket." parameters: - name: dryRun in: query required: false schema: type: string enum: - "1" - "true" description: When `1` / `true`, parse + validate + return the per-row envelope without writing. requestBody: required: true content: text/csv: schema: type: string example: |- type,value,unit,measuredAt,glucoseContext,notes,externalId WEIGHT,80.5,kg,2026-05-01T08:00:00Z,,morning, BLOOD_GLUCOSE,5.3,mmol/L,2026-05-01T08:05:00+02:00,FASTING,,meter-001 BLOOD_GLUCOSE,5.9,mmol/L,2026-05-01T08:20:00+02:00,,,sensor-001 responses: "200": description: Per-row import outcome. content: application/json: schema: $ref: "#/components/schemas/CsvImportResponseEnvelope" "401": *a1 "413": description: CSV exceeds the 16 MB limit. content: application/json: schema: $ref: "#/components/schemas/CsvImportTooLargeEnvelope" "422": *a3 "429": *a4 /api/measurement-reminders: get: tags: - MeasurementReminders summary: List Vorsorge reminders (v1.17.1) description: Returns the owner's live (non-tombstoned) Vorsorge reminders, sorted by server-computed nextDueAt ascending (nulls last). Each row carries the canonical nextDueAt the client renders without recomputing. responses: "200": description: The owner's measurement reminders. content: application/json: schema: $ref: "#/components/schemas/MeasurementReminderListEnvelope" "401": *a1 "422": *a3 "429": *a4 post: tags: - MeasurementReminders summary: Create a Vorsorge reminder (v1.17.1) description: Creates a reminder and computes its server-authoritative nextDueAt. Exactly one of intervalDays (rolling) or rrule (RFC-5545) is required. Wrapped in withIdempotency. 201 on insert. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MeasurementReminderCreate" responses: "201": description: Reminder created. content: application/json: schema: $ref: "#/components/schemas/MeasurementReminderCreatedEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/measurement-reminders/{id}: get: tags: - MeasurementReminders summary: Read a single Vorsorge reminder (v1.17.1) description: Owner-scoped; a cross-user or tombstoned id 404s. parameters: - in: path name: id schema: type: string required: true responses: "200": description: The reminder. content: application/json: schema: $ref: "#/components/schemas/MeasurementReminderEnvelope" "401": *a1 "404": &a9 description: Measurement reminder not found / not owned. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 patch: tags: - MeasurementReminders summary: Edit a Vorsorge reminder (v1.17.1) description: Partial edit; omitted fields are left untouched. nextDueAt is recomputed server-side after the cadence merge. Owner-scoped. parameters: - in: path name: id schema: type: string required: true requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MeasurementReminderUpdate" responses: "200": description: Reminder updated. content: application/json: schema: $ref: "#/components/schemas/MeasurementReminderPatchEnvelope" "401": *a1 "404": *a9 "422": *a3 "429": *a4 delete: tags: - MeasurementReminders summary: Soft-delete a Vorsorge reminder (v1.17.1) description: Sets deletedAt (tombstone). Idempotent. Owner-scoped. parameters: - in: path name: id schema: type: string required: true responses: "200": description: Soft-deleted. content: application/json: schema: $ref: "#/components/schemas/MeasurementReminderDeleteEnvelope" "401": *a1 "404": *a9 "422": *a3 "429": *a4 /api/measurement-reminders/{id}/satisfy: post: tags: - MeasurementReminders summary: Mark a Vorsorge reminder done (v1.17.1) description: "Manual 'Erledigt': stamps lastSatisfiedAt = now and recomputes nextDueAt past now. Free-text reminders resolve only through this path; typed reminders also auto-resolve in the cron when a matching reading lands." parameters: - in: path name: id schema: type: string required: true responses: "200": description: Reminder satisfied; next-due re-anchored. content: application/json: schema: $ref: "#/components/schemas/MeasurementReminderSatisfyEnvelope" "401": *a1 "404": *a9 "422": *a3 "429": *a4 /api/measurement-reminders/{id}/complete: post: tags: - MeasurementReminders summary: Complete a Vorsorge reminder (v1.18.6) description: "Explicit user-action completion (iOS #23): the app marks a reminder done server-side instead of only dismissing it locally. Routes through the same satisfaction primitive as the cron auto-resolve — stamps lastSatisfiedAt = now, re-anchors nextDueAt, fires no notification. Idempotent: completing an already-completed / auto-satisfied reminder returns 200 with completed=false. Owner-scoped." parameters: - in: path name: id schema: type: string required: true responses: "200": description: Completion applied (completed=true) or already-satisfied no-op (completed=false); reminder carries the canonical post-completion DTO. content: application/json: schema: $ref: "#/components/schemas/MeasurementReminderCompleteEnvelope" "401": *a1 "404": *a9 "422": *a3 "429": *a4 /api/labs: get: tags: - Labs summary: List the caller's lab results description: Returns the caller's live (non-deleted) lab results, newest first, with optional analyte / panel / date-range filters. Each row carries a server-computed neutral `rangeStatus` and a `hasNote` flag (the encrypted note itself is not echoed). parameters: - in: query name: biomarkerId schema: type: string minLength: 1 maxLength: 64 - in: query name: analyte schema: type: string minLength: 1 maxLength: 120 - in: query name: panel schema: type: string minLength: 1 maxLength: 120 - 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))$ - 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))$ - in: query name: limit schema: default: 100 type: integer minimum: 1 maximum: 500 - in: query name: offset schema: default: 0 type: integer minimum: 0 maximum: 9007199254740991 - in: query name: sortDir schema: default: desc type: string enum: - asc - desc responses: "200": description: Lab-result list. content: application/json: schema: $ref: "#/components/schemas/ListLabResultsEnvelope" "401": *a1 "422": *a3 "429": *a4 post: tags: - Labs summary: Record a lab result description: Creates a single lab result for the caller. The optional `source` marks provenance (`"MANUAL"` | `"OCR"`); it defaults to `"MANUAL"` when omitted, so the legacy hand-entry contract is unchanged. The on-device-OCR client posts `"OCR"` after the human confirms the parsed values — the raw image never touches the server on this path. The optional note is AES-256-GCM encrypted before write. Audits as `labResult.create`. requestBody: required: true content: application/json: schema: type: object properties: biomarkerId: type: string minLength: 1 maxLength: 64 panel: anyOf: - type: string minLength: 1 maxLength: 120 - type: string const: "" analyte: type: string minLength: 1 maxLength: 120 value: type: number valueText: type: string minLength: 1 maxLength: 120 unit: type: string minLength: 1 maxLength: 40 referenceLow: type: number referenceHigh: type: number takenAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ note: anyOf: - type: string maxLength: 2000 - type: string const: "" source: type: string enum: - MANUAL - OCR required: - takenAt responses: "201": description: Created lab result. content: application/json: schema: $ref: "#/components/schemas/CreateLabResultResponse" "401": *a1 "422": *a3 "429": *a4 /api/labs/{id}: get: tags: - Labs summary: Fetch a single lab result description: Returns the lab result including its decrypted `note`. Cross-user rows surface as 404. parameters: - in: path name: id schema: type: string required: true responses: "200": description: Lab-result detail. content: application/json: schema: $ref: "#/components/schemas/GetLabResultResponse" "401": *a1 "404": &a10 description: Lab result not found (or owned by another user). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 put: tags: - Labs summary: Edit a lab result description: Partial edit; omitted fields are untouched, an explicit null clears `panel` / `note` / a reference bound. Audits as `labResult.update`. parameters: - in: path name: id schema: type: string required: true requestBody: required: true content: application/json: schema: type: object properties: panel: anyOf: - anyOf: - type: string minLength: 1 maxLength: 120 - type: string const: "" - type: "null" analyte: type: string minLength: 1 maxLength: 120 value: type: number valueText: type: string minLength: 1 maxLength: 120 unit: type: string minLength: 1 maxLength: 40 referenceLow: anyOf: - type: number - type: "null" referenceHigh: anyOf: - type: number - type: "null" takenAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ note: anyOf: - anyOf: - type: string maxLength: 2000 - type: string const: "" - type: "null" responses: "200": description: Updated lab result. content: application/json: schema: $ref: "#/components/schemas/UpdateLabResultResponse" "401": *a1 "404": *a10 "422": *a3 "429": *a4 delete: tags: - Labs summary: Delete a lab result description: Soft-deletes the lab result (stamps `deletedAt`). Idempotent. Audits as `labResult.delete`. parameters: - in: path name: id schema: type: string required: true responses: "200": description: Deletion succeeded. content: application/json: schema: $ref: "#/components/schemas/DeleteLabResultResponse" "401": *a1 "404": *a10 "422": *a3 "429": *a4 /api/labs/restore: post: tags: - Labs summary: Restore soft-deleted lab results description: Un-tombstones 1..200 of the caller's soft-deleted lab results in one pass (the delete-Undo affordance). A foreign / live id is a silent no-op. Audits via the wide-event ledger. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RestoreLabResultsRequest" responses: "200": description: Restore count. content: application/json: schema: $ref: "#/components/schemas/RestoreLabResultsResponse" "401": *a1 "422": *a3 "429": *a4 /api/labs/ocr/capability: get: tags: - Labs summary: Probe Lab-OCR availability description: Cheap probe (no provider call) the Labs UI uses to decide whether to show the scan affordance. Reports whether the caller's configured AI provider can read images, why not, and whether PDFs are accepted. responses: "200": description: Capability flags. content: application/json: schema: $ref: "#/components/schemas/OcrCapabilityEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/labs/ocr/extract: post: tags: - Labs summary: Extract lab readings from a photo, PDF, or OCR'd text description: "Read-only (NOT idempotent) extraction. Two modes by content-type. VISION (`multipart/form-data`): a `file` (JPEG/PNG/WebP, or PDF — read natively on Anthropic, else rasterized to page images for any other vision provider; ≤ 12 MiB) is run through the user's vision-capable provider; the upload lives in memory only and is never persisted or logged. TEXT (`application/json`, opt-in local OCR): the browser OCR's the image (tesseract.js) and POSTs `{ mode: \"text\", text }` — only the extracted text reaches the server, so a text-only provider (e.g. ChatGPT-OAuth) reaches the same review/commit flow. Both modes are gated by AI consent, a 6/hour rate bucket, and the per-day token budget, and return proposed rows for the mandatory human review screen — nothing is written. Extracted content is treated as untrusted (prompt-injection); the review step is the safety boundary." requestBody: required: true content: multipart/form-data: schema: type: object properties: file: type: string format: binary description: The lab-report image or PDF. Validated by magic-byte MIME sniff, not the wire Content-Type. required: - file application/json: schema: $ref: "#/components/schemas/OcrTextExtractRequest" responses: "200": description: Proposed rows for review. content: application/json: schema: $ref: "#/components/schemas/OcrExtractEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/labs/ocr/commit: post: tags: - Labs summary: Commit confirmed Lab-OCR rows description: 'Writes ONLY the rows the human confirmed on the review screen. Each row resolves-or-mints a user-scoped biomarker and creates a `LabResult` with `source: "OCR"`; a row that duplicates a live reading is skipped. Idempotent (Idempotency-Key). Audits as `labs.ocr.commit`. `userId` is narrowed from the session, never a body field.' requestBody: required: true content: application/json: schema: type: object properties: rows: minItems: 1 maxItems: 100 type: array items: type: object properties: analyte: type: string minLength: 1 maxLength: 120 panel: anyOf: - type: string minLength: 1 maxLength: 120 - type: string const: "" value: type: number valueText: type: string minLength: 1 maxLength: 120 unit: type: string minLength: 1 maxLength: 40 referenceLow: type: number referenceHigh: type: number takenAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ required: - analyte - takenAt documentId: type: string minLength: 1 maxLength: 64 required: - rows responses: "200": description: Inserted + skipped rows. content: application/json: schema: $ref: "#/components/schemas/OcrCommitEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/biomarkers: get: tags: - Labs summary: List the caller's biomarker catalog description: Returns every biomarker the caller has defined, name-ordered. Each carries its unit, reference bounds, and decrypted context. responses: "200": description: Biomarker catalog. content: application/json: schema: $ref: "#/components/schemas/ListBiomarkersEnvelope" "401": *a1 "422": *a3 "429": *a4 post: tags: - Labs summary: Define a biomarker description: Creates a user-scoped biomarker. The optional context note is AES-256-GCM encrypted before write. Audits as `biomarker.create`. requestBody: required: true content: application/json: schema: type: object properties: name: type: string minLength: 1 maxLength: 120 unit: type: string minLength: 1 maxLength: 40 lowerBound: type: number upperBound: type: number context: type: string maxLength: 2000 panel: anyOf: - type: string minLength: 1 maxLength: 120 - type: string const: "" required: - name - unit responses: "201": description: Created biomarker. content: application/json: schema: $ref: "#/components/schemas/CreateBiomarkerResponse" "401": *a1 "409": &a12 description: A biomarker with this name already exists for the caller. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/biomarkers/{id}: get: tags: - Labs summary: Fetch a single biomarker description: Returns the biomarker including its decrypted `context`. Cross-user rows surface as 404. parameters: - in: path name: id schema: type: string required: true responses: "200": description: Biomarker detail. content: application/json: schema: $ref: "#/components/schemas/GetBiomarkerResponse" "401": *a1 "404": &a11 description: Biomarker not found (or owned by another user). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 put: tags: - Labs summary: Edit a biomarker description: Partial edit; omitted fields are untouched, an explicit null clears `context` / `panel` / a bound. Audits as `biomarker.update`. parameters: - in: path name: id schema: type: string required: true requestBody: required: true content: application/json: schema: type: object properties: name: type: string minLength: 1 maxLength: 120 unit: type: string minLength: 1 maxLength: 40 lowerBound: anyOf: - type: number - type: "null" upperBound: anyOf: - type: number - type: "null" context: anyOf: - type: string maxLength: 2000 - type: "null" panel: anyOf: - anyOf: - type: string minLength: 1 maxLength: 120 - type: string const: "" - type: "null" hidden: type: boolean responses: "200": description: Updated biomarker. content: application/json: schema: $ref: "#/components/schemas/UpdateBiomarkerResponse" "401": *a1 "404": *a11 "409": *a12 "422": *a3 "429": *a4 delete: tags: - Labs summary: Delete a biomarker description: "Hard-deletes the catalog definition. The `onDelete: SetNull` FK unlinks every reading (they keep their legacy `analyte` / `unit`), so the history survives. Audits as `biomarker.delete`." parameters: - in: path name: id schema: type: string required: true responses: "200": description: Deletion succeeded. content: application/json: schema: $ref: "#/components/schemas/DeleteBiomarkerResponse" "401": *a1 "404": *a11 "422": *a3 "429": *a4 /api/illness/episodes: get: tags: - Illness summary: List illness episodes (v1.18.1) description: Returns the caller's live (non-deleted) illness/condition episodes, newest-first by onset. `includeResolved=false` hides episodes that already carry a `resolvedAt`. Born-gated. parameters: - in: query name: limit schema: type: integer minimum: 1 maximum: 100 - in: query name: includeResolved schema: anyOf: - type: string const: "true" - type: string const: "false" responses: "200": description: The caller's illness episodes. content: application/json: schema: $ref: "#/components/schemas/ListIllnessEpisodesEnvelope" "401": *a1 "422": *a3 "429": *a4 post: tags: - Illness summary: Open an illness episode (v1.18.1) description: Creates one episode for the caller. `onsetAt` defaults to 'now' when omitted; the optional note is AES-256-GCM encrypted before write. A `parentConditionId` must reference an owned, live episode. Audits as `illness.episode.create`. Born-gated. requestBody: required: true content: application/json: schema: type: object properties: label: type: string minLength: 1 maxLength: 120 type: type: string enum: - INFECTION - ALLERGY - INJURY - MENTAL_HEALTH - AUTOIMMUNE - CHRONIC - OTHER lifecycle: default: ACUTE type: string enum: - ACUTE - CHRONIC_ONGOING - RECURRING - FLARE onsetAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ resolvedAt: 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" parentConditionId: anyOf: - type: string minLength: 1 maxLength: 40 - type: "null" note: anyOf: - type: string maxLength: 2000 - type: "null" required: - label - type responses: "201": description: Episode created. content: application/json: schema: $ref: "#/components/schemas/CreateIllnessEpisodeEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/illness/episodes/{id}: get: tags: - Illness summary: Read a single illness episode (v1.18.1) description: Returns the episode including its decrypted `note`. Owner-scoped; a cross-user or tombstoned id 404s. Born-gated. parameters: - in: path name: id schema: type: string required: true responses: "200": description: The episode. content: application/json: schema: $ref: "#/components/schemas/GetIllnessEpisodeEnvelope" "401": *a1 "404": &a13 description: Illness episode not found (or owned by another user). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 patch: tags: - Illness summary: Edit an illness episode (v1.18.1) description: Partial edit; omitted fields are left untouched. A re-parent must point at an owned, live episode (and never the episode itself). Audits as `illness.episode.update`. Owner-scoped + module-gated. parameters: - in: path name: id schema: type: string required: true requestBody: required: true content: application/json: schema: type: object properties: label: type: string minLength: 1 maxLength: 120 type: type: string enum: - INFECTION - ALLERGY - INJURY - MENTAL_HEALTH - AUTOIMMUNE - CHRONIC - OTHER lifecycle: type: string enum: - ACUTE - CHRONIC_ONGOING - RECURRING - FLARE onsetAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ resolvedAt: 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" parentConditionId: anyOf: - type: string minLength: 1 maxLength: 40 - type: "null" note: anyOf: - type: string maxLength: 2000 - type: "null" additionalProperties: false responses: "200": description: Episode updated. content: application/json: schema: $ref: "#/components/schemas/UpdateIllnessEpisodeEnvelope" "401": *a1 "404": *a13 "422": *a3 "429": *a4 delete: tags: - Illness summary: Soft-delete an illness episode (v1.18.1) description: "Stamps `deletedAt` (tombstone). Idempotent — a re-delete is a no-op. Returns the `{ deleted: true }` envelope (the cross-module DELETE shape). Audits as `illness.episode.delete`. Owner-scoped + module-gated." parameters: - in: path name: id schema: type: string required: true responses: "200": description: Soft-deleted. content: application/json: schema: $ref: "#/components/schemas/DeleteIllnessEpisodeEnvelope" "401": *a1 "404": *a13 "422": *a3 "429": *a4 /api/illness/episodes/{id}/restore: post: tags: - Illness summary: Restore a soft-deleted illness episode (v1.18.3) description: Clears `deletedAt`, bringing the episode and its preserved day-logs + encrypted note back into every read. A soft-delete keeps the row and children intact, so restore is a pure flag flip — nothing is re-created (the delete-Undo affordance). Idempotent — restoring a live episode is a no-op. Audits as `illness.episode.restore`. Owner-scoped + born-gated. parameters: - in: path name: id schema: type: string required: true responses: "200": description: Episode restored. content: application/json: schema: $ref: "#/components/schemas/RestoreIllnessEpisodeEnvelope" "401": *a1 "404": *a13 "422": *a3 "429": *a4 /api/illness/episodes/{id}/resolve: patch: tags: - Illness summary: Mark an illness episode recovered (v1.18.1) description: Stamps `resolvedAt` (defaults to 'now' when the body omits it). A CHRONIC_ONGOING episode has no recovery date and 422s with `errorCode:"illness.episode.chronic-no-resolve"`. Audits as `illness.episode.resolve`. Owner-scoped + module-gated. parameters: - in: path name: id schema: type: string required: true requestBody: required: false content: application/json: schema: type: object properties: resolvedAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: Episode resolved. content: application/json: schema: $ref: "#/components/schemas/ResolveIllnessEpisodeEnvelope" "401": *a1 "404": *a13 "422": *a3 "429": *a4 /api/illness/episodes/{id}/day-logs: get: tags: - Illness summary: Read an episode's day-log(s) (v1.18.1, list v1.18.3) description: "Two modes on one path, picked by the `date` param. With `date=YYYY-MM-DD`: the single day-log for that day, or `null` when nothing is logged (lets the log-day sheet pre-fill). Without `date`: the date-less LIST — the episode's day-logs newest-first (override with `sortDir`), paged via `limit`/`offset` with `meta.total`. The parent episode must be owned + live. Born-gated." parameters: - in: path name: id schema: type: string required: true - in: query name: date schema: type: string pattern: ^\d{4}-\d{2}-\d{2}$ - 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: sortDir schema: type: string enum: - asc - desc responses: "200": description: "With `date`: the day-log or null. Without `date`: the paged day-log list." content: application/json: schema: $ref: "#/components/schemas/GetIllnessDayLogEnvelope" "401": *a1 "404": *a13 "422": *a3 "429": *a4 post: tags: - Illness summary: Upsert an episode day-log (v1.18.1) description: "Upserts the day's symptom / functional-impact / fever timeline row on `(episodeId, date)`: 201 on insert, 200 on update. The parent episode must be owned + live; the optional note is encrypted at rest. Audits as `illness.day-log.upsert`. Born-gated." parameters: - in: path name: id schema: type: string required: true requestBody: required: true content: application/json: schema: type: object properties: date: type: string pattern: ^\d{4}-\d{2}-\d{2}$ functionalImpact: anyOf: - type: integer minimum: 0 maximum: 3 - type: "null" feverC: anyOf: - type: number minimum: 30 maximum: 45 - type: "null" symptoms: maxItems: 40 type: array items: type: object properties: key: type: string minLength: 1 maxLength: 80 severity: type: integer minimum: 1 maximum: 3 required: - key note: anyOf: - type: string maxLength: 2000 - 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)))$ required: - date responses: "200": description: Existing day-log updated. content: application/json: schema: $ref: "#/components/schemas/UpsertIllnessDayLogUpdatedEnvelope" "201": description: New day-log created. content: application/json: schema: $ref: "#/components/schemas/UpsertIllnessDayLogCreatedEnvelope" "401": *a1 "404": *a13 "422": *a3 "429": *a4 /api/illness/episodes/{id}/correlation: get: tags: - Illness summary: Per-episode retrospective correlation (v1.18.1) description: "Returns the coverage-gated `Derived` correlation for one episode: the pre-onset anomaly scan, the nadir, per-vital physiological returns, the headline recovery-gap, and any red flags. The findings derive from the user's OWN baseline (median ± MAD) over a contamination-guarded window — never a population constant. `status:\"insufficient\"` carries coverage + a reason. Retrospective ONLY — never a predictor or diagnoser. Owner-scoped + module-gated." parameters: - in: path name: id schema: type: string required: true responses: "200": description: The correlation Derived DTO (ok or insufficient). content: application/json: schema: $ref: "#/components/schemas/IllnessCorrelationEnvelope" "401": *a1 "404": *a13 "422": *a3 "429": *a4 /api/illness/insights: get: tags: - Illness summary: Cross-episode retrospective insights (v1.18.1) description: "Returns the cross-episode retrospective summary over a trailing window: 'sick N times · typical recovery gap X days', a recurrence-by-month tally, and a per-type breakdown. The typical gap is withheld (null) below the min-sample floor (asserts nothing thin). Retrospective ONLY — the recurrence figure is a count of the past, never a forecast. Born-gated + owner-scoped." parameters: - in: query name: windowDays schema: type: integer minimum: 30 maximum: 1095 - in: query name: includeRecoveryGap schema: type: string enum: - "true" - "false" responses: "200": description: The cross-episode retrospective summary. content: application/json: schema: $ref: "#/components/schemas/IllnessInsightsEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/mental-health/assessments: get: tags: - Mental health summary: List the caller's screener history description: Returns PHQ-9 / GAD-7 / WHO-5 / SCI administrations (newest first). Totals + bands + flags only; raw item answers are never returned. parameters: - in: query name: instrument schema: type: string enum: - PHQ9 - GAD7 - WHO5 - SCI - in: query name: limit schema: type: integer minimum: 1 maximum: 200 - in: query name: offset schema: type: integer minimum: 0 maximum: 9007199254740991 responses: "200": description: Screener history. content: application/json: schema: $ref: "#/components/schemas/ListMentalHealthAssessmentsEnvelope" "401": *a1 "422": *a3 "429": *a4 post: tags: - Mental health summary: Record a completed screener description: Stores one administration (item answers encrypted) and writes the derived total as a server-owned PHQ9_SCORE / GAD7_SCORE / WHO5_SCORE / SCI_SCORE measurement (source COMPUTED — never client-writable). WHO-5 and SCI totals run HIGHER = better; their follow-up thresholds point downwards (≤ 50 / ≤ 16). On a positive PHQ-9 item-9 the response carries the locale-aware crisis-resource set. requestBody: content: application/json: schema: type: object properties: instrument: type: string enum: - PHQ9 - GAD7 - WHO5 - SCI items: minItems: 1 maxItems: 12 type: array items: type: integer minimum: 0 maximum: 5 functionalDifficulty: type: integer minimum: 0 maximum: 3 takenAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ tz: type: string maxLength: 64 locale: type: string maxLength: 16 source: default: WEB type: string enum: - WEB - IOS externalId: type: string minLength: 1 maxLength: 120 required: - instrument - items responses: "201": description: Assessment recorded. content: application/json: schema: $ref: "#/components/schemas/CreateMentalHealthAssessmentEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/allergies: get: tags: - Records summary: List allergies (v1.25) description: Returns the caller's live (non-deleted) allergy/intolerance records, newest-first. `includeInactive=false` returns only ACTIVE records. parameters: - in: query name: limit schema: type: integer minimum: 1 maximum: 200 - in: query name: includeInactive schema: anyOf: - type: string const: "true" - type: string const: "false" responses: "200": description: The caller's allergy records. content: application/json: schema: $ref: "#/components/schemas/ListAllergiesEnvelope" "401": *a1 "422": *a3 "429": *a4 post: tags: - Records summary: Record an allergy (v1.25) description: Creates one allergy/intolerance record for the caller. The free-text reaction + note are AES-256-GCM encrypted before write. Audits as `allergy.create`. requestBody: required: true content: application/json: schema: type: object properties: substance: type: string minLength: 1 maxLength: 160 category: default: OTHER type: string enum: - FOOD - MEDICATION - ENVIRONMENT - BIOLOGIC - OTHER type: default: ALLERGY type: string enum: - ALLERGY - INTOLERANCE severity: anyOf: - type: string enum: - MILD - MODERATE - SEVERE - type: "null" status: default: ACTIVE type: string enum: - ACTIVE - INACTIVE - RESOLVED onsetAt: 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" reaction: anyOf: - type: string maxLength: 2000 - type: "null" note: anyOf: - type: string maxLength: 2000 - type: "null" required: - substance responses: "201": description: Allergy created. content: application/json: schema: $ref: "#/components/schemas/CreateAllergyEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/allergies/{id}: get: tags: - Records summary: Read a single allergy (v1.25) description: Returns the record including its decrypted `reaction` + `note`. Owner-scoped; a cross-user or tombstoned id 404s. parameters: - in: path name: id schema: type: string required: true responses: "200": description: The allergy record. content: application/json: schema: $ref: "#/components/schemas/GetAllergyEnvelope" "401": *a1 "404": &a14 description: Allergy not found (or owned by another user). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 patch: tags: - Records summary: Edit an allergy (v1.25) description: Partial edit; omitted fields are left untouched. Audits as `allergy.update`. Owner-scoped. parameters: - in: path name: id schema: type: string required: true requestBody: required: true content: application/json: schema: type: object properties: substance: type: string minLength: 1 maxLength: 160 category: type: string enum: - FOOD - MEDICATION - ENVIRONMENT - BIOLOGIC - OTHER type: type: string enum: - ALLERGY - INTOLERANCE severity: anyOf: - type: string enum: - MILD - MODERATE - SEVERE - type: "null" status: type: string enum: - ACTIVE - INACTIVE - RESOLVED onsetAt: 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" reaction: anyOf: - type: string maxLength: 2000 - type: "null" note: anyOf: - type: string maxLength: 2000 - type: "null" additionalProperties: false responses: "200": description: Allergy updated. content: application/json: schema: $ref: "#/components/schemas/UpdateAllergyEnvelope" "401": *a1 "404": *a14 "422": *a3 "429": *a4 delete: tags: - Records summary: Soft-delete an allergy (v1.25) description: "Stamps `deletedAt` (tombstone). Idempotent — a re-delete is a no-op. Returns the `{ deleted: true }` envelope. Audits as `allergy.delete`. Owner-scoped." parameters: - in: path name: id schema: type: string required: true responses: "200": description: Soft-deleted. content: application/json: schema: $ref: "#/components/schemas/DeleteAllergyEnvelope" "401": *a1 "404": *a14 "422": *a3 "429": *a4 /api/family-history: get: tags: - Records summary: List family history (v1.25) description: Returns the caller's live (non-deleted) family-history entries, newest-first. parameters: - in: query name: limit schema: type: integer minimum: 1 maximum: 200 responses: "200": description: The caller's family-history entries. content: application/json: schema: $ref: "#/components/schemas/ListFamilyHistoryEnvelope" "401": *a1 "422": *a3 "429": *a4 post: tags: - Records summary: Record a family-history entry (v1.25) description: Creates one condition-by-relative record for the caller. The free-text note is AES-256-GCM encrypted before write. Audits as `family-history.create`. requestBody: required: true content: application/json: schema: type: object properties: relationship: type: string enum: - MOTHER - FATHER - SISTER - BROTHER - DAUGHTER - SON - GRANDMOTHER_MATERNAL - GRANDFATHER_MATERNAL - GRANDMOTHER_PATERNAL - GRANDFATHER_PATERNAL - AUNT - UNCLE - COUSIN - HALF_SIBLING - OTHER condition: type: string minLength: 1 maxLength: 160 ageAtOnset: anyOf: - type: integer minimum: 0 maximum: 120 - type: "null" note: anyOf: - type: string maxLength: 2000 - type: "null" required: - relationship - condition responses: "201": description: Entry created. content: application/json: schema: $ref: "#/components/schemas/CreateFamilyHistoryEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/family-history/{id}: get: tags: - Records summary: Read a single family-history entry (v1.25) description: Returns the entry including its decrypted `note`. Owner-scoped; a cross-user or tombstoned id 404s. parameters: - in: path name: id schema: type: string required: true responses: "200": description: The family-history entry. content: application/json: schema: $ref: "#/components/schemas/GetFamilyHistoryEnvelope" "401": *a1 "404": &a15 description: Family history entry not found (or owned by another user). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 patch: tags: - Records summary: Edit a family-history entry (v1.25) description: Partial edit; omitted fields are left untouched. Audits as `family-history.update`. Owner-scoped. parameters: - in: path name: id schema: type: string required: true requestBody: required: true content: application/json: schema: type: object properties: relationship: type: string enum: - MOTHER - FATHER - SISTER - BROTHER - DAUGHTER - SON - GRANDMOTHER_MATERNAL - GRANDFATHER_MATERNAL - GRANDMOTHER_PATERNAL - GRANDFATHER_PATERNAL - AUNT - UNCLE - COUSIN - HALF_SIBLING - OTHER condition: type: string minLength: 1 maxLength: 160 ageAtOnset: anyOf: - type: integer minimum: 0 maximum: 120 - type: "null" note: anyOf: - type: string maxLength: 2000 - type: "null" additionalProperties: false responses: "200": description: Entry updated. content: application/json: schema: $ref: "#/components/schemas/UpdateFamilyHistoryEnvelope" "401": *a1 "404": *a15 "422": *a3 "429": *a4 delete: tags: - Records summary: Soft-delete a family-history entry (v1.25) description: "Stamps `deletedAt` (tombstone). Idempotent — a re-delete is a no-op. Returns the `{ deleted: true }` envelope. Audits as `family-history.delete`. Owner-scoped." parameters: - in: path name: id schema: type: string required: true responses: "200": description: Soft-deleted. content: application/json: schema: $ref: "#/components/schemas/DeleteFamilyHistoryEnvelope" "401": *a1 "404": *a15 "422": *a3 "429": *a4 /api/documents/inbound: get: tags: - Documents summary: List stored documents description: The caller's stored documents with title/filename search, category filter, a `documentDate` range, sort, and keyset pagination. Owner-scoped; gated on the opt-in `inboundDocuments` module. parameters: - name: q in: query required: false schema: type: string maxLength: 100 description: Case-insensitive search over title + filename. - name: kind in: query required: false style: form explode: true schema: type: array items: type: string enum: - DOCTOR_REPORT - DISCHARGE_LETTER - LAB_RESULT - IMAGING - PRESCRIPTION - REFERRAL - INSURANCE - VACCINATION - OTHER description: Category filter — OR inside the facet. Repeat the parameter (or send a comma-separated value). - name: episodeId in: query required: false schema: type: string description: Only documents linked to this illness/condition episode. - name: year in: query required: false schema: type: integer minimum: 1900 maximum: 9999 description: Only documents whose filing date falls in this calendar year (UTC). Takes precedence over from/to. - name: from in: query required: false schema: type: string format: date description: Inclusive lower bound on documentDate (YYYY-MM-DD). - name: to in: query required: false schema: type: string format: date description: Inclusive upper bound on documentDate (YYYY-MM-DD). - name: sort in: query required: false schema: type: string enum: - documentDate - createdAt - title default: documentDate - name: order in: query required: false schema: type: string enum: - asc - desc default: desc - name: cursor in: query required: false schema: type: string description: Keyset cursor (the `nextCursor` from a prior page). - name: limit in: query required: false schema: type: integer minimum: 1 maximum: 100 default: 50 responses: "200": description: A page of documents. content: application/json: schema: $ref: "#/components/schemas/InboundDocumentListEnvelope" "401": *a1 "422": *a3 "429": *a4 post: tags: - Documents summary: Store a document (no extraction) description: "STORE-ONLY upload. Stores the raw document ENCRYPTED at rest with `status: STORED` and runs NO extraction — provider-free, no AI consent / budget / egress. A file is always filable, even with no document-scan provider configured. `multipart/form-data`: a `file` plus optional `title`, `kind`, `documentDate` (YYYY-MM-DD), and repeated `episodeIds` form fields (pre-link to the caller's illness/condition episodes). Accepted types (magic-byte sniffed, never the wire Content-Type): PDF/JPEG/PNG/WebP/GIF render inline; Office (docx/xlsx/pptx/doc/xls/ppt), text/CSV/Markdown/RTF, TIFF, HEIC/HEIF, XML/JSON are stored verbatim and served download-only. HEIC is stored as-is but attachment-only — prefer transcoding to JPEG client-side for inline preview parity. Error contract: `413` with `meta.reason = \"fileTooLarge\"` (+ `maxFileBytes`) or `\"quotaExceeded\"` (+ `quotaBytes`, `usedBytes`); `415` with `meta.reason = \"unsupportedType\"`. A same-user duplicate (same bytes) returns 200 + `meta.duplicate: true` with the existing row — not an error. `Idempotency-Key` honoured. Read `GET /api/documents/inbound/usage` for the effective limits before offering an upload. AI extraction is a separate opt-in action — see `POST /api/documents/inbound/{id}/extract`." requestBody: required: true content: multipart/form-data: schema: type: object properties: file: type: string format: binary description: The document. Validated by magic-byte MIME sniff, not the wire Content-Type. title: description: Optional user label. type: string kind: description: Optional category. type: string enum: - DOCTOR_REPORT - DISCHARGE_LETTER - LAB_RESULT - IMAGING - PRESCRIPTION - REFERRAL - INSURANCE - VACCINATION - OTHER documentDate: description: Optional user filing date (YYYY-MM-DD). Defaults to the upload day when omitted so the library sort/filter/display stay aligned. type: string episodeIds: description: Optional condition pre-links (repeat the field per id). Every id must be a live episode of the caller. type: array items: type: string required: - file responses: "200": description: "Duplicate — the same bytes are already stored: the existing row, with `meta.duplicate: true` at the envelope level." content: application/json: schema: $ref: "#/components/schemas/InboundDocumentEnvelope" "201": description: The stored document. content: application/json: schema: $ref: "#/components/schemas/InboundDocumentEnvelope" "401": *a1 "413": description: "Too large: `meta.reason` is `fileTooLarge` (with `maxFileBytes`) or `quotaExceeded` (with `quotaBytes` + `usedBytes`)." content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "415": description: Unsupported or unidentifiable type (`meta.reason = "unsupportedType"`). Executables, HTML/SVG, and generic archives are always refused. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/documents/inbound/capability: get: tags: - Documents summary: Probe document-AI availability + egress description: "Cheap probe (no provider call) the vault uses to decide the AI transport and whether to warn before a read leaves the machine. Resolved over the DOCUMENT provider order (local-first, ChatGPT-subscription OAuth last), so `mode` / `pdfSupported` / `egress` match exactly what the document AI routes do. `egress` is vendor-blind: `\"local\"` (a self-hosted model — the document never leaves the operator's machine) or `\"external\"` (a third-party AI service). The vault shows a per-egress notice before any external document read; sending a document to any external provider also requires an active AI-consent receipt." responses: "200": description: Document-AI capability flags + egress class. content: application/json: schema: $ref: "#/components/schemas/DocumentAiCapabilityEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/documents/inbound/usage: get: tags: - Documents summary: Storage usage + effective limits description: "The caller's vault usage and effective limits: `usedBytes` (every non-purged row — tombstones inside the 30-day undo grace still count), `quotaBytes` (per-user override ?? instance default), `maxFileBytes` (admin-tunable per-file cap), `acceptedExtensions` for picker `accept` lists, and `linkedEpisodes` (episodes carrying at least one live document link — the condition-filter chips). Read this before offering an upload." responses: "200": description: Usage + limits. content: application/json: schema: $ref: "#/components/schemas/DocumentUsageEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/documents/inbound/bulk: post: tags: - Documents summary: Bulk action over documents description: Applies one action to up to 100 owner-scoped documents and returns a per-id result array — a partial failure never aborts the batch. `error` is `notFound` (unknown / foreign / tombstoned where liveness is required) or `conflict` (restore blocked by a live duplicate of the same bytes). requestBody: required: true content: application/json: schema: type: object properties: ids: minItems: 1 maxItems: 100 type: array items: type: string minLength: 1 maxLength: 40 action: type: string enum: - setKind - linkEpisode - unlinkEpisode - delete - restore kind: type: string enum: - DOCTOR_REPORT - DISCHARGE_LETTER - LAB_RESULT - IMAGING - PRESCRIPTION - REFERRAL - INSURANCE - VACCINATION - OTHER episodeId: type: string minLength: 1 maxLength: 40 required: - ids - action responses: "200": description: Per-id outcomes. content: application/json: schema: $ref: "#/components/schemas/DocumentBulkEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/documents/inbound/{id}: get: tags: - Documents summary: Get a document + its staged facts description: The document plus every staged fact for the review screen. Owner-scoped. parameters: - name: id in: path required: true schema: type: string responses: "200": description: Document detail. content: application/json: schema: $ref: "#/components/schemas/InboundDocumentDetailEnvelope" "401": *a1 "422": *a3 "429": *a4 patch: tags: - Documents summary: Edit a document's metadata description: Rename / recategorise / set the user filing date on a stored document. Owner-scoped; no mass assignment. Returns the updated document + its staged facts. parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: title: anyOf: - type: string maxLength: 200 - type: "null" kind: type: string enum: - DOCTOR_REPORT - DISCHARGE_LETTER - LAB_RESULT - IMAGING - PRESCRIPTION - REFERRAL - INSURANCE - VACCINATION - OTHER documentDate: anyOf: - type: string pattern: ^\d{4}-\d{2}-\d{2}$ - type: "null" episodeIds: maxItems: 20 type: array items: type: string minLength: 1 maxLength: 40 responses: "200": description: The updated document. content: application/json: schema: $ref: "#/components/schemas/InboundDocumentUpdateEnvelope" "401": *a1 "422": *a3 "429": *a4 delete: tags: - Documents summary: Discard a document description: Soft-deletes the document + its staging. Facts already approved into the structured stores are independent rows and are NOT affected. parameters: - name: id in: path required: true schema: type: string responses: "200": description: Discarded. content: application/json: schema: $ref: "#/components/schemas/InboundDiscardEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/documents/inbound/{id}/restore: post: tags: - Documents summary: Restore a discarded document description: Clears the soft-delete tombstone within the 30-day undo grace. 409 when the tombstone is already purged (the undo window is over) or when an identical live document blocks the restore (`meta.reason = "duplicateExists"`). Restoring an already-live document is a no-op success. parameters: - name: id in: path required: true schema: type: string responses: "200": description: The restored document + its staged facts. content: application/json: schema: $ref: "#/components/schemas/InboundRestoreEnvelope" "401": *a1 "409": description: "Not restorable: purged past the grace window, or an identical live document exists." content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/documents/inbound/{id}/original: get: tags: - Documents summary: View / download the original document description: "Decrypts and serves the raw uploaded document (the bytes stored encrypted at rest under `InboundDocument.contentEncrypted`). Owner-scoped + gated on the opt-in `inboundDocuments` module. Serving posture follows the document's `servingClass`: inline types (PDF/JPEG/PNG/WebP/GIF) come with their true Content-Type, `Content-Disposition: inline`, `nosniff`, and a `Content-Security-Policy: sandbox` header; attachment types (Office/text/TIFF/HEIC/XML/JSON) ALWAYS come as `application/octet-stream` + `Content-Disposition: attachment` + `nosniff` — never inline. A non-ASCII filename is carried via RFC 5987 `filename*`. Fails closed (500) on a decrypt error — it never returns the ciphertext." parameters: - name: id in: path required: true schema: type: string responses: "200": description: The decrypted original document bytes. Inline types carry the stored MIME type; attachment types are served as application/octet-stream. content: application/pdf: schema: type: string format: binary image/*: schema: type: string format: binary application/octet-stream: schema: type: string format: binary "401": *a1 "422": *a3 "429": *a4 /api/documents/inbound/{id}/thumbnail: get: tags: - Documents summary: View the document preview thumbnail description: "Decrypts and serves the document's small JPEG preview thumbnail (~320px long edge), rendered in the background from the original and stored encrypted at rest under `DocumentThumbnail.thumbnailEncrypted`. Owner-scoped + gated on the opt-in `inboundDocuments` module. Always `Content-Type: image/jpeg` + `nosniff` + `Cache-Control: private, no-store`; loaded as an `` subresource. 404 when no thumbnail exists yet (freshly uploaded / still rendering / unsupported type) — the caller shows a placeholder. Fails closed (500) on a decrypt error — it never returns the ciphertext." parameters: - name: id in: path required: true schema: type: string responses: "200": description: The decrypted JPEG preview thumbnail bytes. content: image/jpeg: schema: type: string format: binary "401": *a1 "422": *a3 "429": *a4 /api/documents/inbound/{id}/extract: post: tags: - Documents summary: Extract facts from a stored document description: "Optional AI enhancement on an already-stored document. Runs the dedicated OCR/vision provider over the stored original and stages STRUCTURED FACTS for review. AI-consent / rate / budget gated. With no provider configured this returns 422 (`documents.inbound.providerUnsupported`) — the stored document is untouched, only the enhancement fails. Two modes by content-type: VISION (empty body) decrypts and scans the stored original (PDF needs an Anthropic vision provider); TEXT (`application/json`, opt-in local OCR) `{ mode: \"text\", text }` structures browser-OCR'd text. Extraction reproduces what the document states — it never interprets. Nothing reaches the structured stores here; the confirm route is the only write path." parameters: - name: id in: path required: true schema: type: string requestBody: required: false content: application/json: schema: $ref: "#/components/schemas/InboundTextExtractRequest" responses: "200": description: The document with its newly staged facts. content: application/json: schema: $ref: "#/components/schemas/InboundExtractEnvelope" "401": *a1 "409": description: "Re-extraction refused: at least one fact on this document is already APPROVED. `meta.errorCode` = `documents.inbound.alreadyPartlyConfirmed`. Re-extracting would sever committed-record provenance and duplicate committed records, so the user must finish reviewing or discard the document first." content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/documents/inbound/{id}/suggest: post: tags: - Documents summary: Suggest filing metadata (drafts only) description: "Optional AI assist on an already-stored document. Runs ONE provider call over the stored original (VISION, empty body) or browser-OCR'd text (TEXT, `application/json` `{ mode: \"text\", text }`, opt-in local OCR) and returns a `{ title, kind, documentDate }` DRAFT for the edit form. AI-consent / rate / budget gated (shares the 6/hour extract bucket). WRITES NOTHING — never stages facts, never flips status; the user reviews and saves. 422 (`documents.inbound.providerUnsupported`) with no provider configured. Never interprets or diagnoses; the title is a neutral filing label." parameters: - name: id in: path required: true schema: type: string requestBody: required: false content: application/json: schema: type: object properties: mode: type: string const: text text: type: string kind: type: string enum: - DOCTOR_REPORT - DISCHARGE_LETTER - LAB_RESULT - IMAGING - PRESCRIPTION - REFERRAL - INSURANCE - VACCINATION - OTHER required: - mode - text responses: "200": description: The filing-metadata suggestion (drafts). content: application/json: schema: $ref: "#/components/schemas/DocumentSuggestEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/documents/inbound/{id}/summary: post: tags: - Documents summary: Summarise or transcribe a document description: 'On-demand description of a stored document. `?mode=summary` (default) returns a short plain-language summary of WHAT the document is; it remains transient unless `persist=true` fills the empty stored-summary slot, while `persist=true&replace=true` explicitly replaces an existing summary. `?mode=text` returns transient raw transcribed text. Neither result reaches coach memory, snapshots, structured stores, or the search index. The summary is descriptive only and never a diagnosis. Same VISION (empty body) / TEXT (`application/json` `{ mode: "text", text }`, opt-in local OCR) dispatch as extract. AI-consent / rate / budget gated. 422 with no provider.' parameters: - name: id in: path required: true schema: type: string - name: mode in: query required: false schema: type: string enum: - summary - text default: summary description: "`summary` (plain-language description) or `text` (raw)." - name: persist in: query required: false schema: type: boolean default: false description: "For summary mode only: persist into an empty stored-summary slot." - name: replace in: query required: false schema: type: boolean default: false description: With `persist=true`, explicitly replace an existing stored summary. requestBody: required: false content: application/json: schema: type: object properties: mode: type: string const: text text: type: string kind: type: string enum: - DOCTOR_REPORT - DISCHARGE_LETTER - LAB_RESULT - IMAGING - PRESCRIPTION - REFERRAL - INSURANCE - VACCINATION - OTHER required: - mode - text responses: "200": description: The summary and its persistence outcome, or transient extracted text. content: application/json: schema: $ref: "#/components/schemas/DocumentSummaryEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/documents/inbound/{id}/index: post: tags: - Documents summary: Build / refresh the content-search index description: "Populates or refreshes one document's content-search index so search matches INSIDE its body. VISION (empty body) decrypts the stored original and runs one provider transcription (AI-consent / rate / budget gated); TEXT (`application/json` `{ mode: \"text\", text }`, opt-in local OCR) indexes browser-OCR'd text with no provider egress. Persists ONLY AES-256-GCM ciphertext of the text plus opaque HMAC token hashes — no plaintext body, no plaintext token. Gated on the existing AI consent (no separate per-user toggle). Idempotent; re-indexing overwrites in place." parameters: - name: id in: path required: true schema: type: string requestBody: required: false content: application/json: schema: type: object properties: mode: type: string const: text text: type: string kind: type: string enum: - DOCTOR_REPORT - DISCHARGE_LETTER - LAB_RESULT - IMAGING - PRESCRIPTION - REFERRAL - INSURANCE - VACCINATION - OTHER required: - mode - text responses: "200": description: The document was indexed. content: application/json: schema: $ref: "#/components/schemas/DocumentIndexEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/documents/inbound/{id}/chat: post: tags: - Documents summary: Chat about a document (streaming reply) description: "v1.27.33 — sends a user turn and streams a grounded prose reply about ONE stored document as Server-Sent Events (`text/event-stream`, not JSON: one `data: \\n\\n` frame per event). Frame `type` is `token` (a chunk of reply text), `done` (`{ type, conversationId, messageId, usage? }`), or `error` (`{ type, code, message }`); HTTP status is 200 even for a provider/refusal outcome (dispatch on the `error` frame). The reply is grounded ONLY in the document's indexed text — NO health snapshot, NO tools, NO other document. The document text is fenced as untrusted DATA (prompt-injection defence); the inbound message + every prior turn are injection-screened and the reply is dose/risk-screened + numerically grounded against the document's own figures. Available only for a content-indexed document (422 `documents.inbound.notIndexed` otherwise). AI-consent-gated (403 `consent.ai.required` for an external provider), budget- and rate-limited. Omitting `conversationId` starts a new thread. Renders as plain text on the client (no markdown). Auth via cookie or Bearer." parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/DocumentChatRequest" responses: "200": description: Server-Sent Events stream of `token` / `done` / `error` frames. content: text/event-stream: schema: type: string description: "SSE frames: `data: \\n\\n`. See the operation description for the per-`type` frame shapes." "401": *a1 "403": description: "AI consent required for an external provider (`errorCode: consent.ai.required`)." content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 get: tags: - Documents summary: Read a document's chat history description: v1.27.33 — with `conversationId`, returns that one thread's messages (decrypted server-side, oldest-first, document- and owner-scoped); without it, the paginated list of the document's chat threads for the sheet's rail. A foreign / unknown id maps to 404 (never 403). Auth via cookie or Bearer. parameters: - name: id in: path required: true schema: type: string - name: conversationId in: query required: false schema: type: string description: When set, returns that thread's messages. - name: cursor in: query required: false schema: type: string description: List pagination cursor (id of the last item). - name: limit in: query required: false schema: type: integer minimum: 1 maximum: 50 responses: "200": description: Either the conversation detail (with `messages`) or the paginated conversation list for the document. content: application/json: schema: $ref: "#/components/schemas/DocumentChatHistoryEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/documents/inbound/reindex: post: tags: - Documents summary: Index all documents for content search description: Enqueues a background job that content-indexes the caller's not-yet-indexed documents (one provider transcription each, bounded + resumable). Gated on the module, a configured vision provider (422 `documents.inbound.providerUnsupported` otherwise), and the existing AI consent (403 otherwise). Returns immediately; the work runs off-request on the queue. responses: "200": description: Whether a backfill job was enqueued. content: application/json: schema: $ref: "#/components/schemas/DocumentReindexEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/documents/inbound/{id}/facts/{factId}: patch: tags: - Documents summary: Edit a staged fact before approval description: Corrects OCR / units / dates / codes on a pending fact. Clears `needsReview` (the values become user-asserted). The fact's resource type cannot change. parameters: - name: id in: path required: true schema: type: string - name: factId in: path required: true schema: type: string requestBody: required: true content: application/json: schema: oneOf: - type: object properties: factType: type: string const: CONDITION label: type: string minLength: 1 maxLength: 300 code: anyOf: - type: string maxLength: 64 - type: "null" codeSystem: anyOf: - type: string enum: - SNOMED - ICD10 - LOINC - RXNORM - ATC - type: "null" clinicalStatus: anyOf: - type: string maxLength: 64 - type: "null" verificationStatus: anyOf: - type: string maxLength: 64 - type: "null" onsetDate: anyOf: - type: string pattern: ^\d{4}-\d{2}-\d{2}$ - type: "null" required: - factType - label - type: object properties: factType: type: string const: OBSERVATION label: type: string minLength: 1 maxLength: 300 code: anyOf: - type: string maxLength: 64 - type: "null" codeSystem: anyOf: - type: string enum: - SNOMED - ICD10 - LOINC - RXNORM - ATC - type: "null" value: anyOf: - type: number - type: "null" valueText: anyOf: - type: string maxLength: 300 - type: "null" unit: anyOf: - type: string maxLength: 80 - type: "null" referenceLow: anyOf: - type: number - type: "null" referenceHigh: anyOf: - type: number - type: "null" effectiveDate: anyOf: - type: string pattern: ^\d{4}-\d{2}-\d{2}$ - type: "null" required: - factType - label - type: object properties: factType: type: string const: MEDICATION_STATEMENT name: type: string minLength: 1 maxLength: 200 dose: anyOf: - type: string maxLength: 120 - type: "null" rxNormCode: anyOf: - type: string maxLength: 20 - type: "null" atcCode: anyOf: - type: string maxLength: 16 - type: "null" statusStated: anyOf: - type: string maxLength: 64 - type: "null" effectiveDate: anyOf: - type: string pattern: ^\d{4}-\d{2}-\d{2}$ - type: "null" required: - factType - name type: object responses: "200": description: The updated fact. content: application/json: schema: $ref: "#/components/schemas/ExtractedFactEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/documents/inbound/{id}/confirm: post: tags: - Documents summary: Confirm the review decisions description: The ONLY write path out of staging. Commits the user's approvals into the structured stores (labs / conditions / medications) and discards rejections. A low-confidence fact that was never edited cannot be approved (fail-closed). Idempotent (Idempotency-Key). Audits as `documents.inbound.confirm`. parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: decisions: minItems: 1 maxItems: 120 type: array items: type: object properties: factId: type: string minLength: 1 maxLength: 40 action: type: string enum: - approve - reject required: - factId - action required: - decisions responses: "200": description: Per-decision outcome. content: application/json: schema: $ref: "#/components/schemas/InboundConfirmEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/custom-metrics: get: tags: - Custom metrics summary: List the caller's custom metrics description: Returns every custom metric the caller has defined, name-ordered, each with its latest logged value and total value count. responses: "200": description: Custom-metric catalog. content: application/json: schema: $ref: "#/components/schemas/ListCustomMetricsEnvelope" "401": *a1 "422": *a3 "429": *a4 post: tags: - Custom metrics summary: Define a custom metric description: Creates a user-scoped custom metric. Re-creating a name that was previously soft-deleted revives that definition. Audits as `customMetric.create`. requestBody: required: true content: application/json: schema: type: object properties: name: type: string minLength: 1 maxLength: 120 unit: type: string minLength: 1 maxLength: 40 targetLow: type: number targetHigh: type: number decimals: type: integer minimum: 0 maximum: 6 description: type: string maxLength: 2000 correlationEnabled: default: false type: boolean required: - name - unit responses: "201": description: Created custom metric. content: application/json: schema: $ref: "#/components/schemas/CreateCustomMetricResponse" "401": *a1 "409": &a17 description: A custom metric with this name already exists for the caller. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/custom-metrics/{id}: get: tags: - Custom metrics summary: Fetch a single custom metric description: Returns the custom-metric definition. Cross-user rows surface as 404. parameters: - in: path name: id schema: type: string required: true responses: "200": description: Custom-metric detail. content: application/json: schema: $ref: "#/components/schemas/GetCustomMetricResponse" "401": *a1 "404": &a16 description: Custom metric not found (or owned by another user). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 patch: tags: - Custom metrics summary: Edit a custom metric description: Partial edit; omitted fields are untouched, an explicit null clears a target bound / decimals / description. Audits as `customMetric.update`. parameters: - in: path name: id schema: type: string required: true requestBody: required: true content: application/json: schema: type: object properties: name: type: string minLength: 1 maxLength: 120 unit: type: string minLength: 1 maxLength: 40 targetLow: anyOf: - type: number - type: "null" targetHigh: anyOf: - type: number - type: "null" decimals: anyOf: - type: integer minimum: 0 maximum: 6 - type: "null" description: anyOf: - type: string maxLength: 2000 - type: "null" correlationEnabled: type: boolean responses: "200": description: Updated custom metric. content: application/json: schema: $ref: "#/components/schemas/UpdateCustomMetricResponse" "401": *a1 "404": *a16 "409": *a17 "422": *a3 "429": *a4 delete: tags: - Custom metrics summary: Delete a custom metric description: Soft-deletes the metric (stamps `deletedAt`); its logged values are retained and re-creating the name revives it. Audits as `customMetric.delete`. parameters: - in: path name: id schema: type: string required: true responses: "200": description: Deletion succeeded. content: application/json: schema: $ref: "#/components/schemas/DeleteCustomMetricResponse" "401": *a1 "404": *a16 "422": *a3 "429": *a4 /api/custom-metrics/{id}/entries: get: tags: - Custom metrics summary: List a custom metric's logged values description: Offset-paginated value feed for one custom metric (the chart + history read). Cross-user / unknown metric ids surface as 404. parameters: - in: path name: id schema: type: string required: true - in: query name: limit schema: type: integer minimum: 1 maximum: 500 - in: query name: offset schema: type: integer minimum: 0 maximum: 9007199254740991 - in: query name: sortDir schema: type: string enum: - asc - desc responses: "200": description: Logged values. content: application/json: schema: $ref: "#/components/schemas/ListCustomMetricEntriesEnvelope" "401": *a1 "404": *a16 "422": *a3 "429": *a4 post: tags: - Custom metrics summary: Log a value description: Records a value against the custom metric, snapshotting the metric's unit. Audits as `customMetricEntry.create`. parameters: - in: path name: id schema: type: string required: true requestBody: required: true content: application/json: schema: type: object properties: 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))$ note: anyOf: - type: string maxLength: 2000 - type: string const: "" required: - value - measuredAt responses: "201": description: Created value. content: application/json: schema: $ref: "#/components/schemas/CreateCustomMetricEntryResponse" "401": *a1 "404": *a16 "422": *a3 "429": *a4 /api/custom-metrics/{id}/entries/{entryId}: patch: tags: - Custom metrics summary: Edit a logged value description: Partial edit of a value; omitted fields untouched, explicit null clears `note`. Audits as `customMetricEntry.update`. parameters: - in: path name: id schema: type: string required: true - in: path name: entryId schema: type: string required: true requestBody: required: true content: application/json: schema: type: object properties: 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))$ note: anyOf: - anyOf: - type: string maxLength: 2000 - type: string const: "" - type: "null" responses: "200": description: Updated value. content: application/json: schema: $ref: "#/components/schemas/UpdateCustomMetricEntryResponse" "401": *a1 "404": &a18 description: Custom metric entry not found (or owned by another user). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 delete: tags: - Custom metrics summary: Delete a logged value description: Hard-deletes the value. Audits as `customMetricEntry.delete`. parameters: - in: path name: id schema: type: string required: true - in: path name: entryId schema: type: string required: true responses: "200": description: Deletion succeeded. content: application/json: schema: $ref: "#/components/schemas/DeleteCustomMetricEntryResponse" "401": *a1 "404": *a18 "422": *a3 "429": *a4 /api/onboarding/disclaimer: post: tags: - Onboarding summary: Acknowledge the one-time medical disclaimer description: "Stamps the user's medical-disclaimer acknowledgment. Idempotent: a repeat acknowledgment of the same version refreshes the timestamp. The body version is a freshness signal so a stale shell cannot record copy it never rendered; the server persists its own canonical version." requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/DisclaimerAckRequest" responses: "200": description: Disclaimer acknowledged. content: application/json: schema: $ref: "#/components/schemas/DisclaimerAckEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/onboarding/tour: post: tags: - Onboarding summary: Update module-tour completion + resume point description: Persists the module-tour state. The client posts a fire-and-forget `progress` checkpoint on each step so a reload resumes at the right module, and a terminal `completed:true` with `outcome` when the tour ends. `completed:false` is a replay reset that also clears the resume point. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/TourUpdateRequest" responses: "200": description: Tour state updated. content: application/json: schema: $ref: "#/components/schemas/TourUpdateEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/nutrients/batch: post: tags: - Nutrients summary: Ingest micronutrient day totals (v1.28) description: >- Bulk upsert of supplement-style daily totals synced from HealthKit — vitamins, minerals, water, caffeine. Day totals only: compute HKStatisticsCollectionQuery cumulative sums day-anchored in the user's current IANA timezone; `day` is yyyy-MM-dd in that timezone (the same rule as the `stats:` day keys). Never post raw samples. Upsert key is (day, nutrient): a re-post replaces the stored total and reports `updated`. Post the current + previous local day per sync; backfill 30 days on first enable. Max 500 entries; Idempotency-Key supported; rate limit 60/min. Per-entry skips (`unit_mismatch` | `value_out_of_range` | `day_invalid`) are terminal — log and drop, do not retry the entry. Behind the opt-in `nutrients` module (403 `module.disabled` when off). Query HealthKit with exactly the listed unit per code (`ug` = microgram; do not convert to IU): | code | HealthKit identifier | unit | | --- | --- | --- | | vitamin_a | HKQuantityTypeIdentifierDietaryVitaminA | ug | | thiamin | HKQuantityTypeIdentifierDietaryThiamin | mg | | riboflavin | HKQuantityTypeIdentifierDietaryRiboflavin | mg | | niacin | HKQuantityTypeIdentifierDietaryNiacin | mg | | pantothenic_acid | HKQuantityTypeIdentifierDietaryPantothenicAcid | mg | | vitamin_b6 | HKQuantityTypeIdentifierDietaryVitaminB6 | mg | | biotin | HKQuantityTypeIdentifierDietaryBiotin | ug | | folate | HKQuantityTypeIdentifierDietaryFolate | ug | | vitamin_b12 | HKQuantityTypeIdentifierDietaryVitaminB12 | ug | | vitamin_c | HKQuantityTypeIdentifierDietaryVitaminC | mg | | vitamin_d | HKQuantityTypeIdentifierDietaryVitaminD | ug | | vitamin_e | HKQuantityTypeIdentifierDietaryVitaminE | mg | | vitamin_k | HKQuantityTypeIdentifierDietaryVitaminK | ug | | calcium | HKQuantityTypeIdentifierDietaryCalcium | mg | | iron | HKQuantityTypeIdentifierDietaryIron | mg | | magnesium | HKQuantityTypeIdentifierDietaryMagnesium | mg | | phosphorus | HKQuantityTypeIdentifierDietaryPhosphorus | mg | | zinc | HKQuantityTypeIdentifierDietaryZinc | mg | | copper | HKQuantityTypeIdentifierDietaryCopper | mg | | manganese | HKQuantityTypeIdentifierDietaryManganese | mg | | selenium | HKQuantityTypeIdentifierDietarySelenium | ug | | chromium | HKQuantityTypeIdentifierDietaryChromium | ug | | molybdenum | HKQuantityTypeIdentifierDietaryMolybdenum | ug | | iodine | HKQuantityTypeIdentifierDietaryIodine | ug | | water | HKQuantityTypeIdentifierDietaryWater | ml | | caffeine | HKQuantityTypeIdentifierDietaryCaffeine | mg | requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/NutrientBatchRequest" responses: "200": description: Batch processed; per-entry statuses inserted | updated | skipped. content: application/json: schema: $ref: "#/components/schemas/NutrientBatchEnvelope" "401": *a1 "403": &a19 description: The opt-in `nutrients` module is off for this account (errorCode `module.disabled`). Stop syncing and do not retry; offer the enable-in-settings hint. Only run the HealthKit read-authorization flow while the module is on. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/nutrients: get: tags: - Nutrients summary: Synced-nutrient window summary (v1.28) description: "Per-nutrient summary of the last `days` days (default 14): latest synced day + total and the days-with-data count, in catalog order. Read-only; feeds the settings card. Behind the opt-in `nutrients` module." parameters: - in: query name: days schema: default: 14 type: integer minimum: 1 maximum: 365 responses: "200": description: The window summary. content: application/json: schema: $ref: "#/components/schemas/NutrientOverviewEnvelope" "401": *a1 "403": *a19 "422": *a3 "429": *a4 /api/nutrients/daily: get: tags: - Nutrients summary: One nutrient's day-bucketed series (v1.29) description: Dense per-day series for one catalog code over the last `days` days (default 30) — one entry per calendar day, 0 for a day with no logged data, summed ACROSS SOURCES (a day may carry both an APPLE_HEALTH row and a MANUAL row since migration 0249). Feeds the `/insights/nutrients` hydration + caffeine charts. Also returns the EFSA reference resolved against the caller's profile sex — `null` when the profile has no sex on file (never guessed). Behind the opt-in `nutrients` module. parameters: - in: query name: nutrient schema: $ref: "#/components/schemas/NutrientCode" required: true description: Closed nutrient catalog code — 24 HealthKit Dietary vitamin/mineral types plus water and caffeine. Energy, macros and sodium/potassium are out of scope. - in: query name: days schema: default: 30 type: integer minimum: 1 maximum: 90 responses: "200": description: The day-bucketed series + resolved reference. content: application/json: schema: $ref: "#/components/schemas/NutrientDailySeriesEnvelope" "401": *a1 "403": *a19 "422": *a3 "429": *a4 /api/nutrients/water: post: tags: - Nutrients summary: Manual water quick-add (v1.29) description: "Writes ONLY the `source=\"MANUAL\"` row for `(day, \"water\")` — never the `source=\"APPLE_HEALTH\"` row the batch endpoint owns, so a manual entry and an Apple sync coexist instead of one clobbering the other. `mode: \"add\"` increments the manual day total (quick-add chips); `mode: \"set\"` overwrites it (the \"edit today's total\" undo path — there is no per-entry ledger). `day` defaults to the caller's current local day when omitted. Idempotency-Key supported; rate limit 60/min. Behind the opt-in `nutrients` module." requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/NutrientWaterWriteRequest" responses: "200": description: The MANUAL water row after the write. content: application/json: schema: $ref: "#/components/schemas/NutrientWaterWriteEnvelope" "401": *a1 "403": *a19 "422": *a3 "429": *a4 /api/integrations/healthkit: get: tags: - Integrations summary: Read the HealthKit integration config description: Returns the resolved per-metric HealthKit config — the default metric set merged with the user's stored overrides — plus the last HealthKit sync instant. Auth via cookie or Bearer. responses: "200": description: Resolved HealthKit config. content: application/json: schema: $ref: "#/components/schemas/HealthKitConfigEnvelope" "401": *a1 "422": *a3 "429": *a4 patch: tags: - Integrations summary: Update the HealthKit integration config description: Merges the supplied entries into the stored config by `id` (unknown ids are ignored) and returns the resolved config (defaults merged) so the client always sees a complete metric list. `lastSyncedAt` is null on the echo. Auth via cookie or Bearer. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/HealthKitConfigPatchRequest" responses: "200": description: Updated + resolved HealthKit config. content: application/json: schema: $ref: "#/components/schemas/HealthKitConfigPatchEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/oura/sync: post: tags: - Integrations summary: Trigger an on-demand Oura sync description: "Runs the same sync entry point the hourly cron uses. No body: the window is derived from the newest row already held, not a client-supplied lookback. Rate-limited 5 requests / 60s per user." responses: "200": description: Sync run complete. content: application/json: schema: $ref: "#/components/schemas/OuraSyncEnvelope" "401": *a1 "422": *a3 "429": *a4 "502": description: The sync failed upstream. The classified failure is already recorded on the `oura` ledger; the response body carries no upstream detail. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" /api/polar/sync: post: tags: - Integrations summary: Trigger an on-demand Polar sync description: "Runs both Polar legs (vitals then workouts) through the same entry point the hourly poll uses, each with its own failure recording. No body: there is no full-history arm to call. Rate-limited 5 requests / 60s per user." responses: "200": description: Sync run complete. content: application/json: schema: $ref: "#/components/schemas/PolarSyncEnvelope" "401": *a1 "422": *a3 "429": *a4 "502": description: The sync failed upstream. The classified failure is already recorded on the `polar` ledger; the response body carries no upstream detail. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" /api/strava/sync: post: tags: - Integrations summary: Trigger an on-demand Strava sync description: "Incremental from the stored Strava cursor (`stravaLastActivityAt` minus an overlap) — a manual run picks up wherever the last one stopped. No body and no full-history arm; deep history belongs to the connect-time backfill queue. `failed` is always `false` on a 200: the underlying walk rethrows on the first row error rather than settling partially, so reaching a 200 means every fetched activity was written. Rate-limited 5 requests / 60s per user." responses: "200": description: Sync run complete. content: application/json: schema: $ref: "#/components/schemas/StravaSyncEnvelope" "401": *a1 "422": *a3 "429": *a4 "502": description: The sync failed upstream. The classified failure is already recorded on the `strava` ledger; the response body carries no upstream detail. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" /api/nightscout/sync: post: tags: - Integrations summary: Trigger an on-demand Nightscout sync description: "Pulls the recent SGV window from the configured Nightscout instance. No body: there is no full-history arm to call. The error body is always a fixed message, never the caught error's own — a Nightscout base URL carries its API token as a query parameter, so an upstream message could leak it. Rate-limited 5 requests / 60s per user." responses: "200": description: Sync run complete. content: application/json: schema: $ref: "#/components/schemas/NightscoutSyncEnvelope" "401": *a1 "422": description: The configured Nightscout origin is private (RFC 1918 / loopback / link-local) and the server operator has not approved private-origin access for this deployment. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "429": *a4 "502": description: The sync failed upstream. The classified failure is already recorded on the `nightscout` ledger; the response body carries no upstream detail. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" /api/google-health/sync/status: get: tags: - Integrations summary: Read the caller's Google Health sync progress description: "Returns only the authenticated user's own bounded current/most-recent sync run; there is no way to address another run or another connection's progress by query parameter (query parameters are ignored by construction). Every field is re-clamped and re-checked against its closed enum on read, so a malformed or legacy stored blob degrades to safe defaults (e.g. `state: \"failed\"`, `resource: \"unknown\"`) rather than reaching the client verbatim. Auth via cookie or Bearer." responses: "200": description: "The current/most-recent run, or `data: null` when none has ever run." content: application/json: schema: $ref: "#/components/schemas/GoogleHealthSyncStatusEnvelope" "401": *a1 "422": *a3 "429": *a4 /api/daily/digest: get: tags: - Insights summary: The unified daily digest description: "Assembles the day's read from already-cached data: the nightly briefing lifted read-only from the insights cache, the dashboard-snapshot health score / meds-today / sleep freshness, plus deterministic integration-status and Vorsorge reads for the 'worth a look' rail. No provider call, no warm-on-mount. Requires the insights module. Cookie or Bearer auth." responses: "200": description: The daily digest. content: application/json: schema: $ref: "#/components/schemas/DailyDigestEnvelope" "401": *a1 "403": *a5 "422": *a3 "429": *a4 /api/daily/digest/dismiss: post: tags: - Insights summary: Dismiss a Today rail item description: "\"Dismiss / mark seen\" for the Today rail's OBSERVATIONAL PriorityItem kinds only — milestone, ecg_new_recording, tension_window. The ACTIONABLE kinds (dose_window, sync_issue, preventive_care, coach_checkin) are structurally unreachable: itemKey must be namespaced under one of the three dismissible kinds or the request 422s before any lookup runs. Persisted server-side so the dismissal survives reload / a second device; an upsert, so a repeat dismiss of the same instance is a no-op. Requires the insights module. Cookie or Bearer auth." requestBody: required: true content: application/json: schema: type: object properties: itemKey: type: string minLength: 1 maxLength: 200 required: - itemKey additionalProperties: false responses: "200": description: The item is dismissed (idempotent). content: application/json: schema: $ref: "#/components/schemas/DismissPriorityItemEnvelope" "401": *a1 "403": *a5 "422": *a3 "429": *a4 /api/dashboard/widgets: get: tags: - Dashboard summary: Read the calling user's dashboard widget layout description: Returns the resolved effective layout (defaults merged in if the user has not customised it) plus the optimistic-concurrency `updatedAt` token. responses: "200": description: The resolved layout (custom or default) plus its token. content: application/json: schema: $ref: "#/components/schemas/DashboardLayout" "401": *a1 "422": *a3 "429": *a4 put: tags: - Dashboard summary: Replace the calling user's dashboard widget layout description: "Persists the layout with preserve-when-absent semantics and returns the normalised layout plus the advanced `updatedAt` token. Optimistic concurrency (v1.32.16): send `baseUpdatedAt` (the token from a prior read) and the write 409s if the stored row changed since — so a committed Save can never be silently reverted by a later request that started from an older snapshot; omit it for the legacy unconditional write. Invalid bodies return the multi-issue 422 envelope." requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/DashboardLayoutPutBody" responses: "200": description: Layout saved; the normalised layout plus the advanced token is echoed back. content: application/json: schema: $ref: "#/components/schemas/DashboardLayoutSaved" "401": *a1 "409": description: Dashboard layout changed since it was loaded (optimistic-concurrency conflict). No write happened. Re-read the resource, re-apply the user's change against the fresh state, and resend with the new token. `meta.errorCode` = `dashboard_layout_conflict`. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a6 "429": *a4 delete: tags: - Dashboard summary: Reset the calling user's dashboard widget layout description: Resets the tile layout and Today-highlight visibility. It preserves the stored comparison baseline, chart overlays, selected score rings, and hero ring order. Idempotent. responses: "200": description: Web-controlled layout settings reset; preserved dashboard preferences are returned with the defaults. content: application/json: schema: $ref: "#/components/schemas/DashboardLayoutReset" "401": *a1 "422": *a3 "429": *a4 /api/auth/me: get: tags: - Auth summary: The signed-in account, and what sharing lets it do description: "Keeps answering while the browser is acting on another record — the switcher, the banner naming whose record is open, and the way back out all read it, so a refusal here would strand a switched session. It is therefore an ACTOR surface: it always serves the caller's own rows, and a request that attaches `X-HealthLog-Account` is refused with 403 `sharing.not_permitted` rather than quietly answered. A client that wants a record's data sends the selector on the read that needs it, never on this call." responses: "200": description: The account payload. content: application/json: schema: $ref: "#/components/schemas/AccountPayloadEnvelope" "401": *a1 "403": description: "A selector header was attached to an actor surface (`meta.errorCode: sharing.not_permitted`)." content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/account/activity: get: tags: - Account sharing summary: Who opened this record, and when description: "Every audit row filed under this account that somebody else performed, newest first — one row per delegate per day with a count, rather than one per request. `retentionDays` rides the response because audit history is purged on a schedule the operator configures: a view that showed the rows without the window would imply a completeness it does not have, and clients must state the number the server sends rather than assume the 365-day default. Not delegable: a grant is not an introduction to the other people in the household." responses: "200": description: The activity, and how far back it can see. content: application/json: schema: $ref: "#/components/schemas/RecordActivityEnvelope" "401": *a1 "403": &a20 description: Refused. `meta.errorCode` is `sharing.not_permitted` when the request was made while acting on another account (grant management is never delegable), or `sharing.access.denied` when the caller may not act as the account they named. The latter is byte-identical for an account that does not exist and one that granted nothing — the refusal is not an enumeration oracle. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/account/grants: get: tags: - Account sharing summary: List grants in both directions description: Every grant this account has given or received, ended ones included. Refused while acting on another account. responses: "200": description: Grants given and received. content: application/json: schema: $ref: "#/components/schemas/AccountGrantListEnvelope" "401": *a1 "403": *a20 "422": *a3 "429": *a4 post: tags: - Account sharing summary: Invite an account to this record description: Offers access to somebody already registered on this instance, at READ or WRITE; the grant confers nothing until they accept it. An identifier that names no account answers 404 — a deliberate disclosure to an authenticated caller on a household instance, rate-limited to 10 invitations an hour, and the alternative (a silent pending row for a mistyped username) is worse. Refused while acting on another account, so a delegate can neither invite nor re-delegate. requestBody: required: true content: application/json: schema: type: object properties: identifier: type: string minLength: 1 maxLength: 255 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" access: default: READ description: "READ can read the record and change nothing. WRITE can additionally ADD entries (readings, results, observations, a medication, a marked dose) and can still edit or delete nothing, including its own. Omitted means READ, so a client that predates the field keeps working. The level is fixed when the invitation is written: no endpoint raises a live grant, because that would widen what the delegate accepted without asking them again. The way up is a new invitation the delegate accepts." type: string enum: - READ - WRITE required: - identifier responses: "201": description: The pending grant. content: application/json: schema: $ref: "#/components/schemas/AccountGrantEnvelope" "401": *a1 "403": *a20 "404": description: No account on this instance carries that identifier. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "409": description: "That account already holds a live grant on this record (`meta.errorCode: sharing.invite.duplicate`). Re-inviting somebody whose access was revoked succeeds and mints a new row; the old one stays as history." content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": description: "Validation failed, or the invitation named the caller's own account (`meta.errorCode: sharing.invite.self`)." content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "429": description: Invitation rate limit exceeded. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" /api/account/grants/{id}/accept: post: tags: - Account sharing summary: Accept an invitation description: "The delegate's own act, and the consent record: it stamps the acceptance time and the accepting IP on the grant row. One-shot and delegate-only — the invited account comes from the session, so an invitation addressed to somebody else answers 404 rather than confirming it exists." parameters: - in: path name: id schema: type: string required: true responses: "200": description: The now-active grant. content: application/json: schema: $ref: "#/components/schemas/AcceptedAccountGrantEnvelope" "401": *a1 "403": *a20 "404": description: No such invitation for this account. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "409": description: Already accepted. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "410": description: The invitation lapsed or was withdrawn. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/account/grants/{id}: delete: tags: - Account sharing summary: Withdraw access (owner) description: "Ends a grant on the caller's own record. No step-up and no confirmation step — reducing access must never be harder than granting it. Enforcement is the delegate's next request, not their next login, and the same transaction puts every browser session of theirs that was inside the record back into their own account. Nothing is deleted: the row survives with `revokedAt` and `revokedBy: GRANTOR`." parameters: - in: path name: id schema: type: string required: true responses: "200": description: The ended grant, and what the session cleanup did. content: application/json: schema: $ref: "#/components/schemas/RevokedAccountGrantEnvelope" "401": *a1 "403": *a20 "404": description: No such grant on this account's record. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "409": description: That access had already ended. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/account/grants/{id}/renounce: post: tags: - Account sharing summary: Hand access back (delegate) description: "The same transition as the owner's revoke, attributed the other way (`revokedBy: GRANTEE`) so the record can tell a withdrawal from a handover. Same session cleanup. Refused while acting on the record being renounced — switch back first (one request), which keeps grant management non-delegable without exceptions." parameters: - in: path name: id schema: type: string required: true responses: "200": description: The ended grant, and what the session cleanup did. content: application/json: schema: $ref: "#/components/schemas/RenouncedAccountGrantEnvelope" "401": *a1 "403": *a20 "404": description: No such grant held by this account. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "409": description: That access had already ended. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 /api/account/switch: post: tags: - Account sharing summary: Act on a granted record, or return to your own description: "Cookie transport only; a Bearer caller gets 400 (`meta.errorCode: sharing.switch.wrong_transport`) and should send the `X-HealthLog-Account` header on the requests it wants scoped instead. The grant is validated here so the client gets an honest refusal immediately, but the stamp authorises nothing on its own — every following request re-checks the grant. `accountId: null` clears the switch and always works, including from inside a switched session: this endpoint is the way out." requestBody: required: true content: application/json: schema: type: object properties: accountId: anyOf: - type: string minLength: 1 maxLength: 64 - type: "null" required: - accountId responses: "200": description: The account this session is now acting on, or null. content: application/json: schema: $ref: "#/components/schemas/AccountSwitchEnvelope" "400": description: Sent over the Bearer transport, which carries its selector per request instead. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "401": *a1 "403": *a20 "422": description: Validation failed. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "429": *a4 components: schemas: EncryptedExportRequest: type: object properties: passphrase: type: string minLength: 12 maxLength: 1024 description: User-chosen passphrase (>= 12 chars). Derives the archive key via Argon2id; never stored, never logged, no server-side recovery. required: - passphrase CheckUserRequest: type: object properties: identifier: type: string minLength: 1 maxLength: 254 description: The typed identifier — either an email or a username. Queried verbatim (no case-folding), never echoed back. required: - identifier description: "Discovery lookup for the iOS onboarding flow: given a typed email or username, resolve the next sign-in step." MfaVerifyRequest: type: object properties: mfaTicket: type: string minLength: 1 description: The opaque single-use ticket from the login response. method: default: totp type: string enum: - totp - recovery code: type: string minLength: 6 maxLength: 32 rememberDevice: description: Opt in to trusting this device for 30 days — subsequent logins skip the second factor (the password is still required). type: boolean required: - mfaTicket - code description: Complete a second-factor login challenge and receive the same token bundle the password path issues. OidcNativeTokenRequest: type: object properties: code: type: string pattern: ^hlh_[A-Za-z0-9_-]{43}$ description: One-time handoff code from the healthlog://oidc-callback?code= redirect. codeVerifier: type: string pattern: ^[A-Za-z0-9\-._~]{43,128}$ description: The app's PKCE code_verifier (RFC 7636). required: - code - codeVerifier description: Exchange a one-time native OIDC handoff code (+ its PKCE verifier) for the standard native token bundle. NativeHandoffTokenRequest: type: object properties: code: type: string pattern: ^hlh_[A-Za-z0-9_-]{43}$ description: One-time handoff code from the healthlog://login-callback?code= redirect. codeVerifier: type: string pattern: ^[A-Za-z0-9\-._~]{43,128}$ description: The app's PKCE code_verifier (RFC 7636). required: - code - codeVerifier description: Exchange a one-time web-handoff code (+ its PKCE verifier) for the standard native token bundle. StepUpOptionsRequest: type: object properties: method: type: string enum: - passkey - webauthn description: "`passkey` begins an assertion against the account's primary passkeys; `webauthn` against its registered second-factor security keys." required: - method StepUpMintRequest: oneOf: - type: object properties: method: type: string const: password password: type: string minLength: 1 maxLength: 512 required: - method - password - type: object properties: method: type: string const: totp code: type: string pattern: ^\d{6}$ required: - method - code - type: object properties: method: type: string const: webauthn challengeId: type: string minLength: 1 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 ceremony response payload. required: - method - challengeId - credential - type: object properties: method: type: string const: passkey challengeId: type: string minLength: 1 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 ceremony response payload. required: - method - challengeId - credential description: Re-prove a factor to mint a single-use, token-bound step-up elevation. `password` reaches the same routes a plain cookie session reaches; `totp`, `webauthn`, and `passkey` additionally satisfy the fresh-factor routes (disable, recovery-code rotation, security-key removal), matching exactly the ceremonies for which the web stamps a session second-factor-verified. type: object MfaDisableRequest: type: object properties: code: type: string minLength: 6 maxLength: 32 method: default: totp type: string enum: - totp - recovery required: - code description: Disable the second factor. Requires fresh step-up plus a current TOTP or recovery code. MfaWebauthnRegisterVerifyRequest: type: object properties: challengeId: type: string minLength: 1 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 ceremony response payload. name: type: string minLength: 1 maxLength: 64 required: - challengeId - credential description: Finish registering a WebAuthn security key as a second factor. MfaWebauthnRenameRequest: type: object properties: name: type: string minLength: 1 maxLength: 64 required: - name MfaWebauthnLoginOptionsRequest: type: object properties: mfaTicket: type: string minLength: 1 description: The opaque single-use ticket from the login response. required: - mfaTicket MfaWebauthnLoginVerifyRequest: type: object properties: mfaTicket: type: string minLength: 1 challengeId: type: string minLength: 1 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 ceremony response payload. rememberDevice: description: Opt in to trusting this device for 30 days — subsequent logins skip the second factor (the password is still required). type: boolean required: - mfaTicket - challengeId - credential description: Complete a second-factor login challenge with a WebAuthn security key and receive the same token bundle the password path issues. 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" syncTrigger: description: "Diagnostic-only. Names what triggered this sync (foreground app open, background refresh, or a push wake). Recorded on the ingest wide event and kept on the account as `lastSyncTrigger` — plus `lastBackgroundSyncAt` for a `background` or `push` trigger — both readable from `GET /api/integrations/healthkit`. It does not affect dedup, attribution, or how any sample is stored. Optional and backward-compatible: pre-#66 clients omit it." type: string enum: - foreground - background - push 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 description: Raw HealthKit reading in Apple's native unit; the server applies any canonical scaling at ingest. For a 10-minute heart-rate bucket (see `externalId`) this is the bucket's AVERAGE bpm; the bucket's spread rides `valueMin` / `valueMax`. valueMin: description: "v1.19.2 (iOS #34 extension) — the bucket's MINIMUM bpm for a 10-minute heart-rate bucket (see `externalId`). Persisted ONLY on a well-formed `stats:HKQuantityTypeIdentifierHeartRate:` row; ignored (stored null) on every other entry. Omit on a pre-v1.19.2 client — the bucket keeps the avg-only contract." type: number valueMax: description: "v1.19.2 (iOS #34 extension) — the bucket's MAXIMUM bpm for a 10-minute heart-rate bucket (see `externalId`). Persisted ONLY on a well-formed `stats:HKQuantityTypeIdentifierHeartRate:` row; ignored (stored null) on every other entry. Omit on a pre-v1.19.2 client — the bucket keeps the avg-only contract." 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: >- Dedup key for the `(userId, type, source, externalId)` composite index. Three shapes: • `HKSample.uuid` — a per-sample reading. First-write-wins: a re-post returns `duplicate` and the stored row is immutable. • `stats::` — a per-day cumulative total (Steps, Active Energy, Sleep Duration, Walking/Running Distance, Flights Climbed). A re-post OVERWRITES the row and returns `updated`. • `stats:HKQuantityTypeIdentifierHeartRate:` — v1.19.0 (iOS #34), refined to 10-minute granularity in v1.30.7, heart-rate bucket carrying the bucket's AVERAGE bpm as one PULSE row. `` is the ISO-8601 UTC 10-minute boundary — zeroed seconds/millis, minutes ∈ {00,10,20,30,40,50}, trailing `Z` (e.g. `2026-06-21T14:10:00.000Z`). A re-post OVERWRITES the row and returns `updated`, so iOS uploads ~144 rows/day instead of one per raw HR sample. A row that targets this prefix with a malformed bucket-start suffix is `skipped` with reason `malformed_hr_bucket_id`. The id MUST be stable across app restarts. An object description or memory address (``, `0x12ab34cd`) is a new value on every launch, so it can never match its own earlier row; such an entry is `skipped` with reason `unstable_external_id` while the rest of the batch still lands. 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" liveActivityPushToken: description: v1.17.1 (#22) hex-encoded ActivityKit Live Activity push token (distinct from the device APNs token). The server addresses a Live Activity update / end push on a medication-intake mutation. Omitted = keep the prior value; null = clear it when the Activity ends. anyOf: - type: string minLength: 8 maxLength: 256 pattern: ^[A-Fa-f0-9]+$ - type: "null" required: - token - bundleId description: Native device registration. Re-registering an APNs token belonging to another user returns 409 (cross-user-hijack guard). BulkMedicationIntakeRequest: type: object properties: entries: minItems: 1 maxItems: 500 type: array items: $ref: "#/components/schemas/BulkMedicationIntakeEntry" required: - entries description: iOS SyncMode bulk intake backfill. 1–500 entries per call; idempotent via `Idempotency-Key` and per-entry `idempotencyKey` / `externalId`. BulkMedicationIntakeEntry: type: object properties: medicationId: type: string minLength: 1 scheduledFor: description: ISO instant; defaults to `takenAt` then now() when omitted. type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: ISO instant of the take; omit + `skipped:false` = pending. type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: description: Default false. type: boolean idempotencyKey: type: string minLength: 1 maxLength: 128 injectionSite: description: Per-entry injection site; a disallowed site marks THIS entry skipped without failing the batch. 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: Pin a taken entry onto a named real scheduled slot; ignored on non-taken entries. type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: Per-entry dose override; persisted only on a taken entry. type: string minLength: 1 maxLength: 50 source: description: v1.28 — Apple Health dose-event import. Must be supplied together with `externalId`, and may only target a medication mirrored from Apple Health (else the whole batch 422s). type: string const: APPLE_HEALTH externalId: description: "The HealthKit dose-event UUID. Drives the idempotent re-sync dedup (first-write-wins per Apple dose). Must be stable across app restarts — an object description or memory address (``, `0x12ab34cd`) is a new value on every launch; such an entry is `skipped` with reason `unstable_external_id` while the rest of the batch still lands." type: string minLength: 1 maxLength: 128 required: - medicationId description: One bulk medication-intake entry. `source` + `externalId` are both-or-neither. 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 reorderLeadDays: description: Per-medication reorder lead time in days (0–60). The low-stock alert fires the supply early enough to reorder before the last dose. Omitted = inherit the user-level reorderLeadDays default (10). type: integer minimum: 0 maximum: 60 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" externalSource: description: "Marks the medication as MIRRORED from an external medication list. Only `APPLE_HEALTH` (the iOS 26+ HealthKit Medications sync) is accepted. Must be supplied together with `externalId`. A mirrored medication is source-exclusive: dose events from that source attach only to it, and the app's own dose-logging UI stays off it." type: string const: APPLE_HEALTH externalId: description: "Stable external identifier of the mirrored medication (the HealthKit `medicationConceptIdentifier`, max 128 chars). Must be supplied together with `externalSource`. Idempotent: re-posting the same pair returns the existing medication with status 200 instead of minting a duplicate. The value MUST be identical across app restarts: an object description or memory address (``, `0x12ab34cd`) is a new value on every launch, never matches its own earlier row, and is refused with 422 rather than minting a duplicate mirror." type: string minLength: 1 maxLength: 128 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. v1.28 — `externalSource` + `externalId` (both-or-neither) mark the medication as mirrored from Apple Health; re-posting the same pair is idempotent and returns the existing medication with status 200.' 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. MedicationListLayoutPutBody: 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 `chartOverlayPrefs` 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 baseUpdatedAt: description: "Optimistic-concurrency base token: the `updatedAt` the client last read for this resource. Omit it for the legacy unconditional write (older clients are unaffected). When present, the write is guarded on the stored row still carrying this exact value — a stale token fails with 409 and changes nothing. A present-but-unparseable value fails with 422 and `meta.errorCode` = `invalid_base_updated_at` — note that this is NOT the unconditional write: sending `null` or a malformed string is rejected rather than treated as an omitted token. Treat as opaque: only ever echo a server-returned value, never parse or synthesise it." type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: - version description: PUT body for the medications list presentation — the presentation fields plus the optional optimistic-concurrency base token (`baseUpdatedAt`). Omit the token for the legacy unconditional write. 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 reorderLeadDays: description: Per-medication reorder lead time in days (0–60). null clears the override and reverts to the user-level reorderLeadDays default. anyOf: - type: integer minimum: 0 maximum: 60 - type: "null" 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). Defaults to OTHER. PEN and AMPOULE are the sealed reservoirs the first dose breaches, so they carry a 30-day post-opening window; every other kind expires only on its printed date. 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" manufacturer: description: Marketing-authorisation holder / maker as printed on the carton. Free text, max 120 chars. anyOf: - type: string maxLength: 120 - type: "null" doseStrength: description: Strength as printed on the container, e.g. "5 mg/0.5 ml". Free text, max 60 chars — deliberately not split into a number + unit because pens state strength per dose, per ml, or per cartridge. anyOf: - type: string maxLength: 60 - 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: Opening date of the container (the user opened it without logging an intake). ACTIVE flips to IN_USE. `null` clears it — the container reads as unopened again, which is how a wrongly auto-opened row is corrected. For a PEN or an AMPOULE this also starts / stops the 30-day in-use clock; other container kinds carry no such clock and expire only on their printed date. 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" 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 manufacturer: description: Marketing-authorisation holder / maker as printed on the carton. Free text, max 120 chars. anyOf: - type: string maxLength: 120 - type: "null" doseStrength: description: Strength as printed on the container, e.g. "5 mg/0.5 ml". Free text, max 60 chars — deliberately not split into a number + unit because pens state strength per dose, per ml, or per cartridge. anyOf: - type: string maxLength: 60 - type: "null" 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" PutCoachPrefsRequest: 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 reminderSuggestions: type: object properties: enabled: default: true type: boolean stopped: default: false type: boolean dismissedCadences: default: [] maxItems: 32 type: array items: type: string maxLength: 64 lastSuggestedAt: default: null anyOf: - type: string maxLength: 40 - type: "null" baseUpdatedAt: description: "Optimistic-concurrency base token: the `updatedAt` the client last read for this resource. Omit it for the legacy unconditional write (older clients are unaffected). When present, the write is guarded on the stored row still carrying this exact value — a stale token fails with 409 and changes nothing. A present-but-unparseable value fails with 422 and `meta.errorCode` = `invalid_base_updated_at` — note that this is NOT the unconditional write: sending `null` or a malformed string is rejected rather than treated as an omitted token. Treat as opaque: only ever echo a server-returned value, never parse or synthesise it." type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: Coach prompt-tuning preferences to persist, plus the optional optimistic-concurrency `baseUpdatedAt` token. The guarded write covers both `coachPrefsJson` and the mirrored `insightsExcludeMetrics` column atomically. Omit the token for the legacy unconditional write. 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. DiabetesFlag: type: object properties: hasDiabetes: type: boolean required: - hasDiabetes description: Per-account diabetes opt-in (v1.18.6). `true` selects the ADA glycemic GOAL bands (fasting 80–130, postprandial < 180) for glucose targets instead of the general non-diabetic bands. A user-declared preference only — never inferred from a value, never a diagnosis. PatchHealthScoreConfigRequest: type: object properties: pillars: maxItems: 7 type: array items: $ref: "#/components/schemas/ScorePillarId" description: The pillars that should count — the positive selection, not the exclusions. Replaces the stored recipe whole; it never merges. An id outside the catalogue is a 422 rather than being dropped, because a client has no business sending one. baseUpdatedAt: description: "Optimistic-concurrency base token: the `updatedAt` the client last read for this resource. Omit it for the legacy unconditional write (older clients are unaffected). When present, the write is guarded on the stored row still carrying this exact value — a stale token fails with 409 and changes nothing. A present-but-unparseable value fails with 422 and `meta.errorCode` = `invalid_base_updated_at` — note that this is NOT the unconditional write: sending `null` or a malformed string is rejected rather than treated as an omitted token. Treat as opaque: only ever echo a server-returned value, never parse or synthesise it." type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: - pillars additionalProperties: false description: "The pillars that should count toward the score. Strict: any key other than `pillars` and `baseUpdatedAt` is rejected." ScorePillarId: type: string enum: - BLOOD_PRESSURE - GLYCAEMIA - ACTIVITY - SLEEP - ADIPOSITY - WELLBEING - LIPIDS description: A Health Score pillar id. ModulePrefsPatchRequest: type: object properties: mood: type: boolean sleep: type: boolean glucose: type: boolean workouts: type: boolean recovery: type: boolean labs: type: boolean illness: type: boolean achievements: type: boolean insights: type: boolean medications: type: boolean doctorReport: type: boolean environment: type: boolean mcp: type: boolean inboundDocuments: type: boolean mentalHealth: type: boolean nutrients: type: boolean baseUpdatedAt: description: "Optimistic-concurrency base token: the `updatedAt` the client last read for this resource. Omit it for the legacy unconditional write (older clients are unaffected). When present, the write is guarded on the stored row still carrying this exact value — a stale token fails with 409 and changes nothing. A present-but-unparseable value fails with 422 and `meta.errorCode` = `invalid_base_updated_at` — note that this is NOT the unconditional write: sending `null` or a malformed string is rejected rather than treated as an omitted token. Treat as opaque: only ever echo a server-returned value, never parse or synthesise it." type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ additionalProperties: false description: Partial module enable/disable update. Each key is an optional boolean; an omitted key is left untouched, `false` disables the module, `true` (re-)enables it. Only the toggleable "secondary domains" are accepted — a core-domain key (`weight`, `bloodPressure`, `pulse`, `medications`) or any unknown key is a 422, so the measurement engine + medications can never be disabled here. `cycle`/`coach` are accepted for forward-compat but their real enabled-state is owned elsewhere (the cycle gate / `disableCoach` + the operator assistant flag). `baseUpdatedAt` is the optional optimistic-concurrency token — omit it for the legacy unconditional write. 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: description: Stored gender. Null or an empty string clears it; an omitted field is left untouched. anyOf: - type: string enum: - MALE - FEMALE - OTHER - 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 dateFormat: description: Date-order display preference. AUTO follows the locale convention, DMY pins day-month-year (dd.MM.yyyy), MDY pins month-day-year (MM/dd/yyyy), YMD pins ISO yyyy-MM-dd. type: string enum: - AUTO - DMY - MDY - YMD 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 and an omitted field is left untouched. An explicit null clears any nullable field; an empty string also clears `displayName`, `dateOfBirth`, `gender`, `fullName`, `insurerName`, `insuranceNumber` and `insurerIkNumber`. `email` is not among them — it is validated as an address, so send null to clear it. `locale`, `timezone`, `timeFormat` and `dateFormat` are not clearable. `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" reorderLeadDays: type: integer minimum: 0 maximum: 60 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 ambientSuggestions: type: boolean nudgeAiComposed: type: boolean measurementReminder: type: object properties: clientManaged: type: boolean baseUpdatedAt: description: "Optimistic-concurrency base token: the `updatedAt` the client last read for this resource. Omit it for the legacy unconditional write (older clients are unaffected). When present, the write is guarded on the stored row still carrying this exact value — a stale token fails with 409 and changes nothing. A present-but-unparseable value fails with 422 and `meta.errorCode` = `invalid_base_updated_at` — note that this is NOT the unconditional write: sending `null` or a malformed string is rejected rather than treated as an omitted token. Treat as opaque: only ever echo a server-returned value, never parse or synthesise it." type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: "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. `baseUpdatedAt` is the optional optimistic-concurrency token: echo it to have the server refuse (409) a write that would silently revert a category another writer set from a fresher read (e.g. iOS `medication.clientManaged`). Omit it for the legacy unconditional write." SavedReportProfile: type: object properties: v: type: number const: 2 leaves: maxItems: 91 type: array items: type: string minLength: 1 maxLength: 64 format: type: string enum: - pdf - fhir - package rangeDays: type: integer minimum: 1 maximum: 365 includeCharts: type: boolean required: - v - leaves - format - rangeDays - includeCharts additionalProperties: false description: "A saved doctor-report scope: which leaves are included plus the export format, trailing window and chart toggle. Replace-only — there is no partial-merge write." FencedCoachChatRequest: type: object properties: conversationId: type: string minLength: 1 maxLength: 64 message: type: string minLength: 1 maxLength: 4000 locale: type: string enum: - en - de attachmentIds: minItems: 1 maxItems: 5 type: array items: type: string minLength: 1 maxLength: 64 required: - message additionalProperties: false CoachAttachmentCreateRequest: type: object properties: documentId: type: string minLength: 1 maxLength: 64 required: - documentId additionalProperties: false InsightsLayoutPutBody: 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 - health-status - breathing - labs-changes - ecg visible: type: boolean order: type: integer minimum: 0 maximum: 99 required: - id - visible - order tiles: minItems: 1 maxItems: 62 type: array items: type: object properties: id: type: string enum: - overview - blood-pressure - pulse - oxygen - body-temperature - respiratory-rate - pain - weight - bmi - body-water - bone-mass - fat-free-mass - fat-mass - muscle-mass - visceral-fat - lean-body-mass - waist - waist-to-height - 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 - grip-strength - 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 - nutrients - 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 baseUpdatedAt: description: "Optimistic-concurrency base token: the `updatedAt` the client last read for this resource. Omit it for the legacy unconditional write (older clients are unaffected). When present, the write is guarded on the stored row still carrying this exact value — a stale token fails with 409 and changes nothing. A present-but-unparseable value fails with 422 and `meta.errorCode` = `invalid_base_updated_at` — note that this is NOT the unconditional write: sending `null` or a malformed string is rejected rather than treated as an omitted token. Treat as opaque: only ever echo a server-returned value, never parse or synthesise it." type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: - version description: PUT body for the Insights layout — the layout fields plus the optional optimistic-concurrency base token (`baseUpdatedAt`). Omit the token for the legacy unconditional write. CoachReminderCreate: type: object properties: note: type: string minLength: 1 maxLength: 280 when: type: string minLength: 1 maxLength: 32 metric: type: string minLength: 1 maxLength: 60 pattern: ^[A-Za-z][A-Za-z0-9_]*$ required: - note additionalProperties: false description: "Create a Coach reminder manually. `note` is free text (the only writable content). `when` is an optional closed grammar token — an ISO date (YYYY-MM-DD), a relative offset (+Nd / +Nw), or a context cue (NEXT_BP_LOGGED, NEXT_WEIGHT_LOGGED, NEXT_SLEEP_LOGGED, NEXT_APP_OPEN) — resolved into the trigger server-side. Omit `when` for a recall-only note. Strict: unknown keys 422." CoachReminderPatch: type: object properties: status: type: string enum: - proposed - active - due - surfaced - done - dismissed when: anyOf: - type: string minLength: 1 maxLength: 32 - type: "null" additionalProperties: false description: "Update a Coach reminder's lifecycle. `status` confirms (proposed → active), or marks done / dismissed. `when` re-schedules via the closed grammar (null clears the due moment). Never carries the note text. Strict: unknown keys 422." CoachSuggestedAction: oneOf: - type: object properties: actionType: type: string const: checkup.create label: type: string minLength: 1 maxLength: 120 interval: type: string enum: - yearly - halfYearly - quarterly - monthly required: - actionType - label - interval - type: object properties: actionType: type: string const: reminder.note note: type: string minLength: 1 maxLength: 280 when: type: string minLength: 1 maxLength: 32 metric: type: string minLength: 1 maxLength: 60 pattern: ^[A-Za-z][A-Za-z0-9_]*$ required: - actionType - note description: Confirm a Coach action card. `actionType` is from the closed allowlist (checkup.create, reminder.note); NEVER a medication or clinical change. `checkup.create` builds a preventive-care Vorsorge reminder (free-text label + a closed interval id resolved to an RRULE server-side); `reminder.note` builds a CoachReminder (note + optional closed `when` grammar + optional metric). Nothing is created without this explicit confirm. type: object 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. EcgIngestRequest: type: object properties: externalRecordingId: type: string minLength: 1 maxLength: 120 description: "The recording's stable id AS THE CLIENT KNOWS IT — for an Apple Health client, the `HKSample.uuid`. It is the source's own identity, not a content hash: re-posting the same id overwrites that recording in place. It does not have to agree with the id the `export.zip` importer derives for the same strip; the server reconciles the two doors itself (see `status: duplicate`)." recordedAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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 strip was recorded on-device, with offset. Part of the recording's identity — see `status: duplicate`." samplingFrequency: type: integer minimum: 1 maximum: 10000 description: Signal sampling rate in Hz (an Apple Watch ECG is 512). Part of the recording's identity, and the divisor for the server-derived `durationSeconds`. samples: minItems: 1 maxItems: 32768 type: array items: type: integer minimum: -1000000 maximum: 1000000 description: The waveform as INTEGER MICRO-VOLTS, in recording order — the same unit the `export.zip` CSV parser produces, so convert from HealthKit's Volts before sending. Capped at 32 768 samples (a clean factor of two above a 30 s / 512 Hz strip). Stored AES-256-GCM encrypted; never persisted as plaintext. lead: description: Recording lead label when the device reports one (e.g. `I`). anyOf: - type: string minLength: 1 maxLength: 40 - type: "null" averageHeartRate: description: The device's average heart rate (BPM) for the strip. anyOf: - type: integer minimum: 1 maximum: 300 - type: "null" classification: description: The RECORDING DEVICE's own verdict, stored verbatim. Only the three ECG values are accepted here. `RhythmClassification` in the database has six members because walking-steadiness and neutral event verdicts share it, and those appear on GET /api/insights/rhythm-events — a decoder generated from this schema must NOT be reused there. HealthLog never reads the waveform to produce or revise a verdict. anyOf: - type: string enum: - IRREGULAR - NOT_DETECTED - INCONCLUSIVE - type: "null" source: type: string enum: - APPLE_HEALTH description: "The client's own source. `APPLE_HEALTH` only: `WITHINGS` rows are minted by the OAuth sync and `COMPUTED` ones by the server, so neither is client-assertable — the same posture POST /api/measurements/batch takes." required: - externalRecordingId - recordedAt - samplingFrequency - samples - source description: One Apple Watch ECG recording. Unknown keys are REJECTED with a 422 naming them rather than silently dropped, because every field here is load-bearing metadata for a stored recording. `sampleCount` and `durationSeconds` are derived server-side and must not be sent. UpdateCorrelationPatternRequest: type: object properties: dismissed: type: boolean required: - dismissed additionalProperties: false BulkMoodRequest: type: object properties: entries: minItems: 1 maxItems: 500 type: array items: $ref: "#/components/schemas/BulkMoodEntry" required: - entries description: iOS SyncMode bulk mood backfill. 1–500 entries per call; per-entry UPSERT keyed by `externalId` so re-runs are idempotent. BulkMoodEntry: type: object properties: mood: type: string enum: - SUPER_GUT - GUT - OKAY - SCHLECHT - LAUSIG tags: maxItems: 20 type: array items: type: string maxLength: 50 tagKeys: description: Structured-tag keys from the catalog; unknown keys dropped. maxItems: 30 type: array items: type: string maxLength: 60 ratedFactors: description: Rated mood factors; an out-of-scale rating marks THIS entry skipped, never the batch. maxItems: 30 type: array items: type: object properties: key: type: string maxLength: 60 rating: type: integer minimum: 1 maximum: 5 required: - key - rating note: type: string maxLength: 500 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)))$ description: ISO instant the entry was logged. source: description: Defaults to MANUAL. type: string enum: - MANUAL - MOODLOG - WEB - TELEGRAM - DAYLIO externalId: description: "iOS-side source-stable id for idempotent dedup on the `(userId, source, externalId)` key. Must be stable across app restarts — an object description or memory address (``, `0x12ab34cd`) is a new value on every launch; such an entry is `skipped` with reason `unstable_external_id` while the rest of the batch still lands." type: string minLength: 1 maxLength: 120 required: - mood - moodLoggedAt description: One bulk mood entry (UPSERT keyed by `externalId`). MoodTagLayoutPutRequest: 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 baseUpdatedAt: description: "Optimistic-concurrency base token: the `updatedAt` the client last read for this resource. Omit it for the legacy unconditional write (older clients are unaffected). When present, the write is guarded on the stored row still carrying this exact value — a stale token fails with 409 and changes nothing. A present-but-unparseable value fails with 422 and `meta.errorCode` = `invalid_base_updated_at` — note that this is NOT the unconditional write: sending `null` or a malformed string is rejected rather than treated as an omitted token. Treat as opaque: only ever echo a server-returned value, never parse or synthesise it." type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: Mood-tag layout to merge (preserve-when-absent), plus the optional optimistic-concurrency `baseUpdatedAt` token. The two manage cards write different fields of the same blob; echo the token to have a stale write refused with 409 instead of resurrecting the other card's overwritten field. Omit it for the legacy unconditional write. MeasurementReminderCreate: type: object properties: label: type: string minLength: 1 maxLength: 120 measurementType: anyOf: - $ref: "#/components/schemas/MeasurementReminderType" - type: "null" intervalDays: anyOf: - type: integer minimum: 1 maximum: 3650 - type: "null" rrule: anyOf: - type: string minLength: 1 maxLength: 512 - type: "null" anchorDate: 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" notifyHour: default: 9 type: integer minimum: 0 maximum: 23 location: anyOf: - type: string maxLength: 200 - type: "null" enabled: default: true type: boolean required: - label description: Create a Vorsorge reminder. Exactly one of intervalDays (rolling, every N days) or rrule (RFC-5545, e.g. FREQ=YEARLY) is required. measurementType enables auto-resolve from an incoming reading; omit it for a free-text checklist reminder. MeasurementReminderType: type: string enum: - WEIGHT - BLOOD_PRESSURE_SYS - PULSE - BLOOD_GLUCOSE - OXYGEN_SATURATION - BODY_TEMPERATURE - BODY_FAT - FAT_MASS - FAT_FREE_MASS - MUSCLE_MASS - LEAN_BODY_MASS - BONE_MASS - TOTAL_BODY_WATER - VISCERAL_FAT - BODY_MASS_INDEX - WAIST_CIRCUMFERENCE - PHQ9_SCORE - GAD7_SCORE - WHO5_SCORE - SCI_SCORE description: Auto-resolve target metric (vitals + body-composition family + the PHQ-9 / GAD-7 / WHO-5 / SCI screenings). BP resolves on BLOOD_PRESSURE_SYS (the SYS row is the 'a BP was measured' sentinel); a screening reminder resolves on the server-written *_SCORE row when a check-in completes. Omit for a free-text Vorsorge that resolves only on a manual satisfy or a matching lab result. MeasurementReminderUpdate: type: object properties: label: type: string minLength: 1 maxLength: 120 measurementType: anyOf: - $ref: "#/components/schemas/MeasurementReminderType" - type: "null" intervalDays: anyOf: - type: integer minimum: 1 maximum: 3650 - type: "null" rrule: anyOf: - type: string minLength: 1 maxLength: 512 - type: "null" anchorDate: 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" notifyHour: type: integer minimum: 0 maximum: 23 location: anyOf: - type: string maxLength: 200 - type: "null" enabled: type: boolean description: Partial edit of a Vorsorge reminder. Omitted fields are left untouched; nextDueAt is recomputed server-side after the patch applies. RestoreLabResultsRequest: type: object properties: ids: minItems: 1 maxItems: 200 type: array items: type: string required: - ids OcrTextExtractRequest: type: object properties: mode: type: string const: text text: type: string description: The in-browser-OCR'd lab-report text. The raw image never reaches the server in this mode. required: - mode - text InboundTextExtractRequest: type: object properties: mode: type: string const: text text: type: string kind: type: string enum: - DOCTOR_REPORT - DISCHARGE_LETTER - LAB_RESULT - IMAGING - PRESCRIPTION - REFERRAL - INSURANCE - VACCINATION - OTHER required: - mode - text DocumentChatRequest: type: object properties: conversationId: type: string minLength: 1 maxLength: 64 message: type: string minLength: 1 maxLength: 4000 locale: type: string enum: - en - de required: - message DisclaimerAckRequest: type: object properties: version: type: string minLength: 1 maxLength: 64 description: The disclaimer copy version the client rendered. A freshness signal only — the server pins and persists its own canonical version. required: - version description: Acknowledge the one-time medical disclaimer shown at onboarding. TourUpdateRequest: type: object properties: completed: description: Flip the coarse completion flag. `false` is a replay reset and clears the stored progress point. type: boolean outcome: description: Informational — distinguishes reaching the end from a skip. type: string enum: - completed - skipped progress: description: Mid-tour resume checkpoint. May arrive alone or with `completed`. $ref: "#/components/schemas/TourProgress" description: Update the module-tour state. Provide `completed` and/or `progress`. TourProgress: type: object properties: lastStopId: anyOf: - type: string minLength: 1 maxLength: 64 - type: "null" completedStopIds: default: [] maxItems: 64 type: array items: type: string minLength: 1 maxLength: 64 status: type: string enum: - in_progress - completed - skipped 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))$ required: - lastStopId - status - updatedAt description: Resumable module-tour progress point. `lastStopId` seeds the resume index; `status` is the running/terminal state. NutrientBatchRequest: type: object properties: entries: minItems: 1 maxItems: 500 type: array items: $ref: "#/components/schemas/NutrientIntakeEntry" required: - entries description: Bulk day-total upsert, max 500 entries. Upsert key is the composite primary key (userId, day, nutrient, source); this route always writes the APPLE_HEALTH source row, so a manual water entry is never clobbered. A re-post replaces the stored total (last-writer-wins day-total contract). NutrientIntakeEntry: type: object properties: day: type: string pattern: ^\d{4}-\d{2}-\d{2}$ nutrient: $ref: "#/components/schemas/NutrientCode" unit: type: string minLength: 1 maxLength: 10 amount: type: number minimum: 0 externalSourceVersion: type: string minLength: 1 maxLength: 120 required: - day - nutrient - unit - amount description: One nutrient day total. day = YYYY-MM-DD in the user's IANA timezone; unit must equal the catalog's canonical unit for the code (mg | ug | ml) or the entry is skipped unit_mismatch. NutrientCode: type: string enum: - vitamin_a - thiamin - riboflavin - niacin - pantothenic_acid - vitamin_b6 - biotin - folate - vitamin_b12 - vitamin_c - vitamin_d - vitamin_e - vitamin_k - calcium - iron - magnesium - phosphorus - zinc - copper - manganese - selenium - chromium - molybdenum - iodine - water - caffeine description: Closed nutrient catalog code — 24 HealthKit Dietary vitamin/mineral types plus water and caffeine. Energy, macros and sodium/potassium are out of scope. NutrientWaterWriteRequest: type: object properties: amountMl: type: number minimum: 1 maximum: 20000 mode: type: string enum: - add - set day: type: string pattern: ^\d{4}-\d{2}-\d{2}$ required: - amountMl - mode description: Manual water quick-add against the MANUAL source row. add increments today's manual total; set overwrites it. Never touches the APPLE_HEALTH row. HealthKitConfigPatchRequest: type: object properties: entries: maxItems: 50 type: array items: type: object properties: id: type: string minLength: 1 maxLength: 64 kind: type: string minLength: 1 maxLength: 64 direction: type: string enum: - bidirectional - readOnly - writeOnly - disabled description: Per-metric sync direction. enabled: type: boolean required: - id - direction required: - entries description: Merge-by-`id` update of the HealthKit metric config. Unknown ids are silently ignored; omitted fields fall back to the stored (or default) value for that entry. Up to 50 entries per call. DashboardLayoutPutBody: type: object properties: version: type: number const: 1 widgets: minItems: 1 maxItems: 50 type: array items: type: object properties: id: type: string enum: - weight - bp - pulse - bodyFat - mood - medications - sleep - steps - glucose - totalBodyWater - boneMass - muscleMass - bpInTarget - oxygenSaturation - hrv - respiratoryRate - achievements - vo2Max - recentWorkouts - cardioRecovery - sixMinuteWalk - stairAscentSpeed - stairDescentSpeed - breathingDisturbances - wristTemperature - falls - walkingSteadiness - vorsorge - waterIntake - restingHeartRate - walkingSpeed - walkingAsymmetry - walkingStepLength - bmi - bodyTemperature - walkingDoubleSupport - audioExposureEnvironment - audioExposureHeadphone - gripStrength - painNRS - waistCircumference - waistToHeight visible: type: boolean tileVisible: type: boolean order: type: integer minimum: 0 maximum: 99 required: - id - visible - order comparisonBaseline: type: string enum: - none - lastMonth - lastYear chartOverlayPrefs: type: object propertyNames: type: string enum: - bp - weight - bmi - pulse - bodyFat - mood - medications - sleep - steps - vo2Max - hrv - restingHr - oxygenSaturation - bodyTemperature - activeEnergy - bloodGlucose - totalBodyWater - boneMass - flightsClimbed - walkingRunningDistance - fatFreeMass - fatMass - muscleMass - skinTemperature - pulseWaveVelocity - vascularAge - visceralFat - audioExposureEnv - audioExposureHeadphone - timeInDaylight - walkingSteadiness - audioExposureEvent - respiratoryRate - leanBodyMass - walkingHeartRateAverage - walkingAsymmetry - walkingDoubleSupport - walkingStepLength - walkingSpeed - cardioRecovery - wristTemperature - fallCount - sixMinuteWalkDistance - stairAscentSpeed - stairDescentSpeed - breathingDisturbances - ansCharge - dayStrain - workoutStrain - cardioLoad - averageHeartRate - maxHeartRate - energyExpenditureKj - gripStrength - painNrs - waistCircumference - waistToHeight additionalProperties: type: object properties: showTrendIndicator: type: boolean showTrendArrow: type: boolean showTargetRange: type: boolean comparisonBaseline: type: string enum: - none - lastMonth - lastYear rangePoints: anyOf: - type: number const: 0 - type: number const: 7 - type: number const: 30 - type: number const: 90 required: - showTrendIndicator - showTrendArrow - showTargetRange selectedScoreRings: maxItems: 3 type: array items: type: string enum: - READINESS - RECOVERY_SCORE - SLEEP_SCORE - MED_COMPLIANCE heroRingOrder: maxItems: 4 type: array items: type: string enum: - HEALTH_SCORE - READINESS - RECOVERY_SCORE - SLEEP_SCORE - MED_COMPLIANCE enabledHeroItemKinds: description: Today-highlight item kinds in catalogue order. Missing means every current kind is enabled; an empty array disables all kinds. maxItems: 8 type: array items: type: string enum: - coach_checkin - dose_window - preventive_care - sync_issue - milestone - ecg_new_recording - tension_window - same_time_baseline baseUpdatedAt: description: "Optimistic-concurrency base token: the `updatedAt` the client last read for this resource. Omit it for the legacy unconditional write (older clients are unaffected). When present, the write is guarded on the stored row still carrying this exact value — a stale token fails with 409 and changes nothing. A present-but-unparseable value fails with 422 and `meta.errorCode` = `invalid_base_updated_at` — note that this is NOT the unconditional write: sending `null` or a malformed string is rejected rather than treated as an omitted token. Treat as opaque: only ever echo a server-returned value, never parse or synthesise it." type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: - version description: PUT body for the dashboard layout — the layout fields plus the optional optimistic-concurrency base token (`baseUpdatedAt`). Omit the token for the legacy unconditional write. 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" verdict: $ref: "#/components/schemas/CycleVerdictDTO" 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 - verdict - 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 CycleVerdictDTO: type: object properties: state: type: string enum: - IN_CYCLE - OVERDUE - INSUFFICIENT_DATA dayOfCycle: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: 1-based day of the current cycle. Null in OVERDUE and INSUFFICIENT_DATA — beyond the typical length plus the server's grace window the count is no longer an observed fact. cycleLength: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: "Days the ring represents: the observed labelled run, or the profile-derived idealized cycle for a low-data tracker." phase: anyOf: - $ref: "#/components/schemas/CyclePhase" - type: "null" spans: type: array items: type: object properties: phase: $ref: "#/components/schemas/CyclePhase" fraction: type: number required: - phase - fraction additionalProperties: false cycleStartDate: anyOf: - type: string - type: "null" description: First day of the current cycle (YYYY-MM-DD). Still set while OVERDUE — the last logged period start remains a fact once the count stops. overdueDays: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: How many days past the profile's typical cycle length the open cycle has run. Set only in OVERDUE. daysUntilNext: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: Days from today to the predicted next period start. Null when no prediction ran and null once that start is in the past. fertileWindow: type: object properties: start: anyOf: - type: string - type: "null" end: anyOf: - type: string - type: "null" active: type: boolean required: - start - end - active additionalProperties: false required: - state - dayOfCycle - cycleLength - phase - spans - cycleStartDate - overdueDays - daysUntilNext - fertileWindow additionalProperties: false description: The resolved cycle verdict for the user's own timezone day. Render these values; do not recompute them. The grace window that decides OVERDUE is deliberately not published — read `state` and `overdueDays`. 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 MeasurementCategoriesEnvelope: type: object properties: data: $ref: "#/components/schemas/MeasurementCategoriesResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MeasurementCategoriesResponse: type: object properties: version: type: number const: 1 description: Additive schema marker; assignments never reshuffle. categories: type: array items: type: object properties: id: type: string description: Category id (e.g. `vitals`). labelKey: type: string description: i18n key for the category label (`categories.`). order: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Stable display order. required: - id - labelKey - order additionalProperties: false description: The ordered category list for the picker. assignments: type: object propertyNames: type: string additionalProperties: type: string description: MeasurementType → category id map. required: - version - categories - assignments additionalProperties: false description: "The measurement categorisation overlay: the ordered category list plus the MeasurementType → category assignments. Derived server-side from the canonical map; cached `public, max-age=600`." 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. scopeMintable: type: boolean description: "Whether a narrow Bearer token for the FHIR face can be obtained. Always false: no mint path grants the scope, so the face is reachable by the owner's cookie session or a wildcard device token only." 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 - scopeMintable - 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. reportDownload: type: array items: type: string description: Machine formats a share link serves as a download under its token, behind the same unlock as the page. selectionVersion: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Version of the leaf-selection grammar this build speaks. groups: type: array items: type: object properties: id: type: string description: Group id. leaves: type: array items: type: string description: Member leaf ids, in the order the panel renders them. sensitive: description: "Present and true only on the fenced tier: no single control may switch on more than one of its leaves." type: boolean required: - id - leaves additionalProperties: false description: Selection groups, in presentation order, each carrying its member leaf ids. leaves: type: array items: type: string description: Every leaf id a report selection may carry. Membership is inclusion; absence is exclusion. required: - supported - maxDays - reportDownload - selectionVersion - groups - leaves additionalProperties: false description: Clinician share-link surface descriptor. 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" protected: type: boolean documentCount: type: number documentOnly: type: boolean needsReselection: 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. passphrase: type: string description: Raw passphrase second factor — returned ONCE; stored only as an HMAC hash, unrecoverable. shareUrl: type: string description: Absolute `/c/` URL (no passphrase). qrUrl: type: string description: Absolute share URL with the passphrase in the URL FRAGMENT (`#k=`). Encode this as the QR payload; the fragment is never sent to the server. required: - id - label - rangeStart - rangeEnd - protected - documentCount - documentOnly - needsReselection - expiresAt - createdAt - revokedAt - lastAccessAt - accessCount - active - token - passphrase - shareUrl - qrUrl 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" protected: type: boolean documentCount: type: number documentOnly: type: boolean needsReselection: 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 - protected - documentCount - documentOnly - needsReselection - 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 ShareLinkUnlock: type: object properties: data: type: object properties: unlocked: type: boolean const: true required: - unlocked additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CheckUserEnvelope: type: object properties: data: $ref: "#/components/schemas/CheckUserResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CheckUserResponse: type: object properties: branch: type: string enum: - not_found - passkey_only - email_fallback - exists description: "Next UX step: `not_found` (show sign-up), `passkey_only` (Sign in with Passkey), `email_fallback` (password field, with a Passkey affordance when applicable), `exists` (account with no usable credential — recovery path)." hasPasskey: type: boolean description: The account has at least one registered passkey. hasPassword: type: boolean description: The account has a password hash. required: - branch - hasPasskey - hasPassword additionalProperties: false description: Account-existence + credential shape. The response is identical whether or not the identifier matched (account-existence is the explicit contract iOS needs); the identifier is never echoed. 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: description: Access token (`hlk_<64hex>`); only present for native-policy callers. A browser caller gets the session cookie instead. type: string 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: - user additionalProperties: false description: Native-client token bundle returned by login + refresh. Web cookie-only callers see only `user`. MfaRequiredResponse: type: object properties: data: type: "null" error: type: "null" meta: type: object properties: mfaRequired: type: boolean const: true mfaTicket: type: string description: Opaque, single-use, ~5-minute ticket to present to /api/auth/mfa/verify. methods: type: array items: type: string enum: - totp - recovery - webauthn description: Second factors the account can complete the challenge with. `webauthn` is completed via /api/auth/mfa/webauthn/verify; the rest via /api/auth/mfa/verify. required: - mfaRequired - mfaTicket - methods additionalProperties: false required: - data - error - meta additionalProperties: false description: Password accepted but a second factor is required. Not an error and not a session — no token is issued until /api/auth/mfa/verify succeeds. MfaVerifyResponse: 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 OidcNativeTokenResponse: 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 NativeHandoffTokenResponse: 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 StepUpOptionsEnvelope: type: object properties: data: $ref: "#/components/schemas/StepUpOptionsResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false StepUpOptionsResponse: type: object properties: options: type: object propertyNames: type: string additionalProperties: {} description: SimpleWebAuthn assertion options for the caller's passkeys. challengeId: type: string required: - options - challengeId additionalProperties: false StepUpMintEnvelope: type: object properties: data: $ref: "#/components/schemas/StepUpMintResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false StepUpMintResponse: type: object properties: elevation: type: string description: Opaque single-use elevation (`hle_<64hex>`). Present it in the `X-Step-Up` header on ONE second-factor-management call. Returned exactly once; store it in memory only. 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)))$ expiresInSeconds: type: integer minimum: -9007199254740991 maximum: 9007199254740991 method: type: string enum: - password - totp - webauthn - passkey description: The factor that was re-proved. satisfiesFreshFactor: type: boolean description: Whether this elevation reaches the fresh-factor routes (MFA disable, recovery-code regeneration, security-key removal). False for a password proof — mirroring the web, where a password login never marks a session second-factor-verified. required: - elevation - expiresAt - expiresInSeconds - method - satisfiesFreshFactor additionalProperties: false TotpSetupEnvelope: type: object properties: data: $ref: "#/components/schemas/TotpSetupResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false TotpSetupResponse: type: object properties: otpauthUri: type: string description: otpauth:// URI to render as a QR code (carries the secret). totpSecret: type: string description: Base32 secret for manual entry. Pending until confirmed. required: - otpauthUri - totpSecret additionalProperties: false TotpConfirmEnvelope: type: object properties: data: $ref: "#/components/schemas/MfaRecoveryCodesResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MfaRecoveryCodesResponse: type: object properties: enabled: type: boolean recoveryCodes: type: array items: type: string description: Single-use recovery codes, shown once. Save them now. recoveryCodesRemaining: type: integer minimum: -9007199254740991 maximum: 9007199254740991 required: - recoveryCodes - recoveryCodesRemaining additionalProperties: false MfaDisableEnvelope: type: object properties: data: $ref: "#/components/schemas/MfaToggleResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MfaToggleResponse: type: object properties: enabled: type: boolean required: - enabled additionalProperties: false MfaRecoveryRegenEnvelope: type: object properties: data: $ref: "#/components/schemas/MfaRecoveryCodesResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MfaStatusEnvelope: type: object properties: data: $ref: "#/components/schemas/MfaStatusResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MfaStatusResponse: type: object properties: totp: type: object properties: enabled: type: boolean required: - enabled additionalProperties: false recoveryCodesRemaining: type: integer minimum: -9007199254740991 maximum: 9007199254740991 webauthn: type: array items: $ref: "#/components/schemas/WebauthnMfaCredentialInfo" passkeyNudgeDismissed: type: boolean required: - totp - recoveryCodesRemaining - webauthn - passkeyNudgeDismissed additionalProperties: false WebauthnMfaCredentialInfo: type: object properties: id: type: string name: 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)))$ lastUsedAt: 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 - name - createdAt - lastUsedAt additionalProperties: false MfaWebauthnRegisterOptionsEnvelope: type: object properties: data: $ref: "#/components/schemas/WebauthnOptionsResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false WebauthnOptionsResponse: type: object properties: options: type: object propertyNames: type: string additionalProperties: {} description: SimpleWebAuthn options to pass to the browser ceremony. challengeId: type: string description: Server-issued challenge id. required: - options - challengeId additionalProperties: false MfaWebauthnRegisterVerifyEnvelope: type: object properties: data: $ref: "#/components/schemas/WebauthnMfaCredentialInfo" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MfaWebauthnRenameEnvelope: type: object properties: data: $ref: "#/components/schemas/WebauthnMfaCredentialInfo" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MfaWebauthnRemoveEnvelope: type: object properties: data: $ref: "#/components/schemas/AuthSuccessFlagResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false AuthSuccessFlagResponse: type: object properties: success: type: boolean required: - success additionalProperties: false MfaWebauthnLoginOptionsEnvelope: type: object properties: data: $ref: "#/components/schemas/WebauthnOptionsResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MfaWebauthnVerifyResponse: 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 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 RefreshRevokeResponse: 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 PasskeyListEnvelope: type: object properties: data: $ref: "#/components/schemas/PasskeyListResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false PasskeyListResponse: type: array items: $ref: "#/components/schemas/PasskeyInfo" PasskeyInfo: type: object properties: id: type: string description: Row id; the handle for DELETE. name: type: string description: User-supplied label, defaulting to “Passkey”. credentialDeviceType: type: string description: "Authenticator device type as reported at registration: `singleDevice` for a credential bound to one authenticator, `multiDevice` for a syncable one." credentialBackedUp: type: boolean description: Whether the authenticator reported the credential as backed up (synced to the platform keychain) at registration time. Not re-read afterwards. 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)))$ description: Registered at. lastUsedAt: 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 this passkey last completed a verified assertion; null when it has never been used to sign in. Stamped on every successful login, so a client can render a real 'last used' rather than assuming never. required: - id - name - credentialDeviceType - credentialBackedUp - createdAt - lastUsedAt additionalProperties: false SessionListEnvelope: type: object properties: data: $ref: "#/components/schemas/SessionListResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false SessionListResponse: type: object properties: sessions: type: array items: type: object properties: id: type: string device: type: string description: Coarse device label derived from the User-Agent. ipMasked: anyOf: - type: string - type: "null" description: IP with the host portion masked — never the full address. location: anyOf: - type: string - type: "null" description: Resolved coarse location, when available. lastActiveAt: 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" 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)))$ isCurrent: type: boolean description: True for the session making this request. required: - id - device - ipMasked - location - lastActiveAt - createdAt - isCurrent additionalProperties: false required: - sessions additionalProperties: false SignOutEverywhereEnvelope: type: object properties: data: $ref: "#/components/schemas/SignOutEverywhereResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false SignOutEverywhereResponse: type: object properties: sessionsRevoked: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Number of OTHER sessions removed (the current one is kept). required: - sessionsRevoked additionalProperties: false SessionRevokeEnvelope: 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 TrustedDeviceListEnvelope: type: object properties: data: $ref: "#/components/schemas/TrustedDeviceListResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false TrustedDeviceListResponse: type: object properties: devices: type: array items: type: object properties: id: type: string label: anyOf: - type: string - type: "null" description: Coarse, IP-free device label (e.g. 'Firefox on macOS'). 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)))$ lastUsedAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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)))$ isCurrent: type: boolean description: True for the device making this request. required: - id - label - createdAt - lastUsedAt - expiresAt - isCurrent additionalProperties: false required: - devices additionalProperties: false TrustedDeviceRevokeAllEnvelope: type: object properties: data: type: object properties: revoked: type: integer minimum: -9007199254740991 maximum: 9007199254740991 required: - revoked additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false TrustedDeviceRevokeEnvelope: 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 SecurityActivityEnvelope: type: object properties: data: $ref: "#/components/schemas/SecurityActivityResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false SecurityActivityResponse: type: object properties: events: type: array items: type: object properties: action: type: string description: Audit action name, e.g. auth.login.password. 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)))$ location: anyOf: - type: string - type: "null" ipMasked: anyOf: - type: string - type: "null" description: Host-masked IP. carrier: anyOf: - type: string - type: "null" required: - action - createdAt - location - ipMasked - carrier additionalProperties: false required: - events 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. Reports the server-side `lastSyncedAt` checkpoint, which the batch ingest endpoints set when a client push lands; the handshake itself writes nothing. 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 description: Stored row id, or a synthetic key (`day::`, `sleep-seg::`) on the collapsed list modes. 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 - ANS_CHARGE - CARDIO_LOAD - SLEEP_SCORE - BODY_TEMPERATURE_DEVIATION - RESILIENCE - PHQ9_SCORE - GAD7_SCORE - GRIP_STRENGTH - PAIN_NRS - WAIST_CIRCUMFERENCE - WAIST_TO_HEIGHT - WHO5_SCORE - SCI_SCORE 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 - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA notes: anyOf: - type: string - type: "null" userId: type: string valueMin: description: Bucket MINIMUM — set only on an hourly heart-rate `stats:` row. anyOf: - type: number - type: "null" valueMax: description: Bucket MAXIMUM — set only on an hourly heart-rate `stats:` row. anyOf: - type: number - type: "null" externalId: anyOf: - type: string - type: "null" description: Cross-device dedup key (UUID string or `stats::`). externalSourceVersion: anyOf: - type: string - type: "null" glucoseContext: description: Set only on BLOOD_GLUCOSE rows, and null there too when the writer recorded no classification (a sensor export, a device sync). Never invent one. anyOf: - type: string - type: "null" sleepStage: description: Set only on SLEEP_DURATION rows. anyOf: - type: string - type: "null" rhythmClassification: description: Device classification verdict; set only on EVENT rows. anyOf: - type: string - type: "null" deviceType: description: "`watch | band | ring | phone | scale | other | unknown`." anyOf: - type: string - type: "null" syncVersion: type: integer exclusiveMinimum: 0 maximum: 9007199254740991 description: LWW reconciliation counter; echo to keep the mirror monotonic. deletedAt: description: Soft-delete tombstone; null on a live row. 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" 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)))$ dayKey: type: string sampleCount: type: integer minimum: -9007199254740991 maximum: 9007199254740991 partial: description: True on the one bucket whose day was cut off by the read cap, so its sum understates the real total. type: boolean 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. GetMeasurementsSeriesBatchResponse: type: object properties: data: $ref: "#/components/schemas/MeasurementsSeriesBatchResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MeasurementsSeriesBatchResponse: type: object properties: series: type: object propertyNames: 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 - ANS_CHARGE - CARDIO_LOAD - SLEEP_SCORE - BODY_TEMPERATURE_DEVIATION - RESILIENCE - PHQ9_SCORE - GAD7_SCORE - GRIP_STRENGTH - PAIN_NRS - WAIST_CIRCUMFERENCE - WAIST_TO_HEIGHT - WHO5_SCORE - SCI_SCORE additionalProperties: type: array items: 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 - ANS_CHARGE - CARDIO_LOAD - SLEEP_SCORE - BODY_TEMPERATURE_DEVIATION - RESILIENCE - PHQ9_SCORE - GAD7_SCORE - GRIP_STRENGTH - PAIN_NRS - WAIST_CIRCUMFERENCE - WAIST_TO_HEIGHT - WHO5_SCORE - SCI_SCORE value: type: number description: Daily mean (spot metrics) or SUM (cumulative). 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)))$ description: Day-bucket start (ISO-8601). count: description: Underlying raw-row count for the bucket. type: integer minimum: 0 maximum: 9007199254740991 minValue: description: Per-day minimum (spot metrics only; absent for cumulative). anyOf: - type: number - type: "null" maxValue: description: Per-day maximum (spot metrics only; absent for cumulative). anyOf: - type: number - type: "null" required: - type - value - measuredAt additionalProperties: false required: - 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 - ANS_CHARGE - CARDIO_LOAD - SLEEP_SCORE - BODY_TEMPERATURE_DEVIATION - RESILIENCE - PHQ9_SCORE - GAD7_SCORE - GRIP_STRENGTH - PAIN_NRS - WAIST_CIRCUMFERENCE - WAIST_TO_HEIGHT - WHO5_SCORE - SCI_SCORE description: Per-type daily series, keyed by MeasurementType. required: - series additionalProperties: false description: Batched daily series — one rollup-backed series per requested type. 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" valueMin: description: "v1.19.2 (iOS #34 extension) — per-point MINIMUM for `kind=pulse`. On an aggregated hourly heart-rate bucket `value` is the hour's average and this is the hour's low; null on a per-sample PULSE row and absent for every other kind." anyOf: - type: number - type: "null" valueMax: description: "v1.19.2 (iOS #34 extension) — per-point MAXIMUM for `kind=pulse`. On an aggregated hourly heart-rate bucket `value` is the hour's average and this is the hour's high; null on a per-sample PULSE row and absent for every other kind." anyOf: - 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: anyOf: - $ref: "#/components/schemas/MeasurementResource" - $ref: "#/components/schemas/AggregatedMeasurementBucket" meta: $ref: "#/components/schemas/ListMeasurementsMeta" required: - measurements - meta 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 description: Stored row id, or a synthetic key (`day::`, `sleep-seg::`) on the collapsed list modes. 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 - ANS_CHARGE - CARDIO_LOAD - SLEEP_SCORE - BODY_TEMPERATURE_DEVIATION - RESILIENCE - PHQ9_SCORE - GAD7_SCORE - GRIP_STRENGTH - PAIN_NRS - WAIST_CIRCUMFERENCE - WAIST_TO_HEIGHT - WHO5_SCORE - SCI_SCORE 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 - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA notes: anyOf: - type: string - type: "null" userId: type: string valueMin: description: Bucket MINIMUM — set only on an hourly heart-rate `stats:` row. anyOf: - type: number - type: "null" valueMax: description: Bucket MAXIMUM — set only on an hourly heart-rate `stats:` row. anyOf: - type: number - type: "null" externalId: description: External-system dedup key; null for manual entries. anyOf: - type: string - type: "null" externalSourceVersion: anyOf: - type: string - type: "null" glucoseContext: description: Set only on BLOOD_GLUCOSE rows, and null there too when the writer recorded no classification (a sensor export, a device sync). Never invent one. anyOf: - type: string - type: "null" sleepStage: description: Set only on SLEEP_DURATION rows. anyOf: - type: string - type: "null" rhythmClassification: description: Device classification verdict; set only on EVENT rows. anyOf: - type: string - type: "null" deviceType: description: "`watch | band | ring | phone | scale | other | unknown`." anyOf: - type: string - type: "null" syncVersion: description: Bumped on every server-side mutation; the last-writer-wins input for cross-device reconciliation. type: integer minimum: -9007199254740991 maximum: 9007199254740991 deletedAt: description: Soft-delete tombstone; null on a live row. 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" 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)))$ dayKey: type: string sampleCount: type: integer minimum: -9007199254740991 maximum: 9007199254740991 partial: description: True on the one bucket whose day was cut off by the read cap, so its sum understates the real total. type: boolean required: - id - type - value - unit - measuredAt - source additionalProperties: false description: Server-shaped measurement row. GET endpoints return the whole stored row with the decrypted note on `notes` and the `notesEncrypted` ciphertext stripped; the collapsed list modes synthesise rows carrying the display fields plus `dayKey` / `sampleCount`. AggregatedMeasurementBucket: 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 - ANS_CHARGE - CARDIO_LOAD - SLEEP_SCORE - BODY_TEMPERATURE_DEVIATION - RESILIENCE - PHQ9_SCORE - GAD7_SCORE - GRIP_STRENGTH - PAIN_NRS - WAIST_CIRCUMFERENCE - WAIST_TO_HEIGHT - WHO5_SCORE - SCI_SCORE value: type: number description: The bucket's aggregated value. 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)))$ description: Bucket start. count: type: integer minimum: 0 maximum: 9007199254740991 unit: type: string required: - type - value - measuredAt additionalProperties: false description: Pre-folded bucket returned by the `aggregate=` list modes instead of a stored row. ListMeasurementsMeta: type: object properties: total: type: integer minimum: 0 maximum: 9007199254740991 limit: type: integer minimum: 0 maximum: 9007199254740991 offset: type: integer minimum: 0 maximum: 9007199254740991 dayKey: description: Echoed on the per-sample drill-down modes. type: string groupBy: description: Set on the collapsed day-sum / per-night modes. type: string enum: - day - night aggregate: description: Set on the aggregate modes — the requested grain. type: string droppedDuplicates: description: Rows discarded by the cross-source canonical picker on the day-collapse mode. type: integer minimum: 0 maximum: 9007199254740991 required: - total - limit - offset additionalProperties: false CreateMeasurementResponse: type: object properties: data: anyOf: - $ref: "#/components/schemas/MeasurementResource" - type: array items: $ref: "#/components/schemas/MeasurementResource" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false GetMeasurementResponse: 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 UpdateMeasurementResponse: 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 DeleteMeasurementResponse: type: object properties: data: type: object properties: deleted: type: boolean const: true required: - deleted additionalProperties: false 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: processed: type: integer minimum: 0 maximum: 9007199254740991 inserted: type: integer minimum: 0 maximum: 9007199254740991 updated: type: integer minimum: 0 maximum: 9007199254740991 duplicates: type: integer minimum: 0 maximum: 9007199254740991 failed: 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/AppleHealthBatchEntryResult" required: - processed - inserted - updated - duplicates - failed - skipped - entries additionalProperties: false AppleHealthBatchEntryResult: type: object properties: index: type: integer minimum: 0 maximum: 9007199254740991 status: type: string enum: - inserted - updated - duplicate - skipped - failed description: "`inserted`/`duplicate` — the row landed (advance the cursor). `updated` — a `stats:` aggregate overwrote an existing row. `skipped` — validation no-op; see `reason`. `failed` — retryable database failure that must not advance the entry cursor; the response is marked `Cache-Control: no-store`." 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 - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA - 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 reconstructed: type: boolean description: True when the source has no per-stage onset timestamps and the server synthesised a contiguous timeline in a fixed physiological order (WHOOP). The client renders the hypnogram but labels it an approximate layout and never recomputes. False for real-series sources (Apple Health / Withings / Fitbit). 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 sourceDiscrepancy: anyOf: - type: object properties: deltaMinutes: type: integer minimum: 0 maximum: 9007199254740991 description: Spread (max − min) of the disagreeing per-writer asleep totals, in minutes. sources: type: array items: type: object properties: source: type: string description: MeasurementSource value of the writer bucket. deviceType: anyOf: - type: string - type: "null" asleepMinutes: type: integer minimum: 0 maximum: 9007199254740991 required: - source - deviceType - asleepMinutes additionalProperties: false required: - deltaMinutes - sources additionalProperties: false - type: "null" description: Non-null when two writer buckets reported clearly different asleep totals for this session (> 45 min apart and > 20% of the larger total). Observational only — the served totals stay the winning writer's; clients may show a discreet 'sources disagree' hint listing each bucket's total. required: - night - source - start - end - asleepMinutes - inBedMinutes - awakeMinutes - awakenings - reconstructed - stages - segments - sourceDiscrepancy 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 - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA - 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 reconstructed: type: boolean description: True when the source has no per-stage onset timestamps and the server synthesised a contiguous timeline in a fixed physiological order (WHOOP). The client renders the hypnogram but labels it an approximate layout and never recomputes. False for real-series sources (Apple Health / Withings / Fitbit). 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 sourceDiscrepancy: anyOf: - type: object properties: deltaMinutes: type: integer minimum: 0 maximum: 9007199254740991 description: Spread (max − min) of the disagreeing per-writer asleep totals, in minutes. sources: type: array items: type: object properties: source: type: string description: MeasurementSource value of the writer bucket. deviceType: anyOf: - type: string - type: "null" asleepMinutes: type: integer minimum: 0 maximum: 9007199254740991 required: - source - deviceType - asleepMinutes additionalProperties: false required: - deltaMinutes - sources additionalProperties: false - type: "null" description: Non-null when two writer buckets reported clearly different asleep totals for this session (> 45 min apart and > 20% of the larger total). Observational only — the served totals stay the winning writer's; clients may show a discreet 'sources disagree' hint listing each bucket's total. required: - night - source - start - end - asleepMinutes - inBedMinutes - awakeMinutes - awakenings - reconstructed - stages - segments - sourceDiscrepancy 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." SleepRhythmResponse: type: object properties: data: $ref: "#/components/schemas/SleepRhythmResource" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false SleepRhythmResource: type: object properties: sleepDebt: type: object properties: state: type: string enum: - partial - ready description: "`partial` until enough tracked nights exist (calm 'still learning'); `ready` once the rolling window can assert a balance." debtMinutes: type: integer minimum: 0 maximum: 9007199254740991 description: Outstanding sleep-debt balance over the window, in minutes, after caps. A night below need adds its shortfall; a night above need pays the balance down (floored at 0). needMinutes: type: integer minimum: 0 maximum: 9007199254740991 description: Age-based sleep need (minutes) used for the per-night deficit. nightsCounted: type: integer minimum: 0 maximum: 9007199254740991 windowNights: type: integer exclusiveMinimum: 0 maximum: 9007199254740991 description: Rolling window length in nights. nightsUntilReady: type: integer minimum: 0 maximum: 9007199254740991 description: Nights still needed before the debt is asserted (0 when ready). source: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA description: v1.25.0 — the active source the debt is resolved FROM, picked off the user's `sleepDebt` source ladder. `COMPUTED` is HealthLog's own rolling-balance estimate (the only producer today); a provider value would be a device-native debt. Clients explain the figure when it is `COMPUTED`. required: - state - debtMinutes - needMinutes - nightsCounted - windowNights - nightsUntilReady - source additionalProperties: false chronotype: type: object properties: state: type: string enum: - learning - ready description: "`learning` until enough free-day nights exist (no band asserted); `ready` once the free-day sample is large enough." msfMinutes: anyOf: - type: number - type: "null" description: Mid-sleep on free days (minutes-of-day), null while learning. msfScMinutes: anyOf: - type: number - type: "null" description: Sleep-debt-corrected mid-sleep on free days (MSFsc, minutes-of-day), null while learning. band: anyOf: - type: string enum: - extreme_early - early - intermediate - late - extreme_late - type: "null" description: MCTQ chronotype band off MSFsc, null while learning. socialJetlagMinutes: anyOf: - type: number - type: "null" description: Social jetlag = circular |MSF_work − MSF_free| in minutes, null when one side is missing. freeNightsCounted: type: integer minimum: 0 maximum: 9007199254740991 workNightsCounted: type: integer minimum: 0 maximum: 9007199254740991 freeNightsUntilReady: type: integer minimum: 0 maximum: 9007199254740991 description: Free-day nights still needed before a band is asserted. required: - state - msfMinutes - msfScMinutes - band - socialJetlagMinutes - freeNightsCounted - workNightsCounted - freeNightsUntilReady additionalProperties: false averagePerNight: type: object properties: state: type: string enum: - partial - ready averageMinutes: type: integer minimum: 0 maximum: 9007199254740991 description: Mean asleep minutes per scorable night (0 while partial). nightsCounted: type: integer minimum: 0 maximum: 9007199254740991 nightsUntilReady: type: integer minimum: 0 maximum: 9007199254740991 description: Scorable nights still needed before the average is asserted. required: - state - averageMinutes - nightsCounted - nightsUntilReady additionalProperties: false required: - sleepDebt - chronotype - averagePerNight additionalProperties: false description: "Server-authoritative sleep-rhythm read: the outstanding sleep-debt balance over the rolling window + MCTQ chronotype (MSF/MSFsc band, social jetlag). Free vs work nights default to weekend = free in the user's timezone (no work calendar). A view over existing per-stage SLEEP_DURATION rows — no schema, no new type." 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 - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA externalId: anyOf: - type: string - type: "null" hasRoute: type: boolean hasHrSeries: type: boolean required: - id - sportType - startedAt - endedAt - durationSec - distanceM - activeEnergyKcal - avgHr - maxHr - source - externalId - hasRoute - hasHrSeries 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)))$ dayKey: type: string 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 - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA 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" hrSeries: anyOf: - $ref: "#/components/schemas/WorkoutHrCurve" - type: "null" zones: anyOf: - $ref: "#/components/schemas/WorkoutZones" - type: "null" splits: anyOf: - type: array items: $ref: "#/components/schemas/WorkoutSplit" - type: "null" sportContext: anyOf: - $ref: "#/components/schemas/WorkoutSportContext" - type: "null" aiInsight: anyOf: - $ref: "#/components/schemas/WorkoutActivityInsight" - type: "null" canonicalId: type: string previousWorkoutId: anyOf: - type: string - type: "null" required: - id - sportType - startedAt - endedAt - dayKey - durationSec - distanceM - activeEnergyKcal - avgHr - maxHr - minHr - stepCount - elevationM - pauseDurationSec - source - externalId - metadata - route - samples - hrSeries - zones - splits - sportContext - aiInsight - canonicalId - previousWorkoutId 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 WorkoutHrCurve: type: object properties: source: type: string enum: - workout_series - pulse_window bucketSec: type: integer exclusiveMinimum: 0 maximum: 9007199254740991 points: type: array items: type: object properties: tSec: type: integer minimum: 0 maximum: 9007199254740991 mean: type: integer minimum: -9007199254740991 maximum: 9007199254740991 min: type: integer minimum: -9007199254740991 maximum: 9007199254740991 max: type: integer minimum: -9007199254740991 maximum: 9007199254740991 required: - tSec - mean - min - max additionalProperties: false envelope: type: boolean required: - source - bucketSec - points - envelope additionalProperties: false WorkoutZones: type: object properties: model: type: string enum: - whoop - tanaka hrMax: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" zones: type: array items: type: object properties: zone: type: integer minimum: 1 maximum: 5 lowBpm: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" highBpm: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" seconds: type: integer minimum: 0 maximum: 9007199254740991 required: - zone - lowBpm - highBpm - seconds additionalProperties: false required: - model - hrMax - zones additionalProperties: false WorkoutSplit: type: object properties: km: type: integer exclusiveMinimum: 0 maximum: 9007199254740991 durationSec: type: integer minimum: 0 maximum: 9007199254740991 paceSecPerKm: type: integer minimum: 0 maximum: 9007199254740991 required: - km - durationSec - paceSecPerKm additionalProperties: false WorkoutSportContext: type: object properties: count: type: integer minimum: 0 maximum: 9007199254740991 avgDurationSec: type: integer minimum: 0 maximum: 9007199254740991 avgDistanceM: anyOf: - type: number - type: "null" avgAvgHr: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" required: - count - avgDurationSec - avgDistanceM - avgAvgHr additionalProperties: false WorkoutActivityInsight: type: object properties: paragraph: type: string 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: - paragraph - generatedAt 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 BulkMedicationIntakeResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/BulkMedicationIntakeResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false BulkMedicationIntakeResponse: type: object properties: processed: type: integer minimum: 0 maximum: 9007199254740991 inserted: type: integer minimum: 0 maximum: 9007199254740991 updated: 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/BulkMedicationIntakeEntryResult" required: - processed - inserted - updated - duplicates - skipped - entries additionalProperties: false BulkMedicationIntakeEntryResult: type: object properties: index: type: integer minimum: 0 maximum: 9007199254740991 status: type: string enum: - inserted - updated - duplicate - skipped description: "`inserted`/`updated`/`duplicate` — the row landed (advance the cursor). `skipped` — not stored; see `reason`." reason: type: string id: description: The landed row id (absent on skips). type: string required: - index - status 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. reorderLeadDays: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: v1.17.0 — optional per-medication reorder lead time in days (0–60). The low-stock alert widens its trigger by this lead plus one dose-interval so a refill arrives before the last dose. null = inherit the user-level notificationPrefs.medication.reorderLeadDays default (10). 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" nextDueOverdue: type: boolean 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" externalSource: anyOf: - type: string const: APPLE_HEALTH - type: "null" description: v1.32.25 — provenance of the medication. `APPLE_HEALTH` marks the row a read-only mirror of the iOS 26+ HealthKit Medications sync; NULL is a native HealthLog medication. Read-only echo of the stored provenance — the create route sets it, the list read surfaces it so mirrored-vs-native is visible without inspecting the iOS client's local registry. 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: number - 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 - reorderLeadDays - active - notificationsEnabled - liveActivityEnabled - criticalAlarmEnabled - atcCode - rxNormCode - pausedAt - snoozedUntil - nextDueAt - nextDueOverdue - startsOn - endsOn - oneShot - asNeeded - createdAt - updatedAt - schedules - category - externalSource - lastTakenAt - todayEventCount - stockUnitsRemaining - stockDosesRemaining additionalProperties: false description: List-row variant of the medication resource enriched with the joined `category`, the v1.32.25 `externalSource` provenance echo, `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. reorderLeadDays: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: v1.17.0 — optional per-medication reorder lead time in days (0–60). The low-stock alert widens its trigger by this lead plus one dose-interval so a refill arrives before the last dose. null = inherit the user-level notificationPrefs.medication.reorderLeadDays default (10). 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: description: "Present on the READ paths only — the list and single GET compute it; the create / update responses return the stored row and omit it. 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." 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" nextDueOverdue: description: "Present on the READ paths only, alongside `nextDueAt`. 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." type: boolean 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 - reorderLeadDays - active - notificationsEnabled - liveActivityEnabled - criticalAlarmEnabled - atcCode - rxNormCode - pausedAt - snoozedUntil - 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/MedicationListLayoutResult" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MedicationListLayoutResult: 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 `chartOverlayPrefs` 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 updatedAt: description: "Optimistic-concurrency token: the stored row's `updatedAt` at read/write time. Echo it back as `baseUpdatedAt` on the next write. Opaque." type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: - version additionalProperties: false description: Resolved medications list presentation plus the optimistic-concurrency `updatedAt` token. MedicationListLayoutSaved: type: object properties: data: $ref: "#/components/schemas/MedicationListLayoutResult" 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/MedicationListLayoutResult" 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 - APPLE_HEALTH 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" summary: $ref: "#/components/schemas/MedicationSupplySummary" meta: type: object properties: total: type: integer minimum: 0 maximum: 9007199254740991 required: - total additionalProperties: false required: - items - summary - 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 (on a PEN / AMPOULE the 30-day in-use clock is running); EXPIRED = printed expiry lapsed, or the in-use window lapsed on a clock-bearing container, 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); defaults to OTHER. PEN and AMPOULE are the sealed reservoirs the first dose breaches, so opening one starts a 30-day post-opening window; BLISTER / INHALER / BOTTLE / OTHER hold individually sealed units and expire only on their printed date. unitsTotal: anyOf: - type: number - type: "null" 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`. v1.18.3 (iOS#31) — NULL when the unit count is unknown (a corrupt or legacy row); the client renders "unknown", never a fabricated 0. unitsRemaining: anyOf: - type: number - type: "null" 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. v1.18.3 (iOS#31) — NULL when the count is unknown (a corrupt or legacy row); the client renders "unknown", never a fabricated 0 it could decrement into negatives. 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. On a PEN / AMPOULE it starts the 30-day in-use clock; on the other kinds it only records that the pack is open. 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) for a clock-bearing container (PEN / AMPOULE), else printedExpiry alone. NULL when no deadline applies — an opened blister with no printed date has none. 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" manufacturer: anyOf: - type: string - type: "null" description: Marketing-authorisation holder / maker as printed on the carton. NULL for a container registered before the field existed, and for plain supply rows that never carried one. doseStrength: anyOf: - type: string - type: "null" description: Strength as printed on the container, e.g. "5 mg/0.5 ml". Free text, not split into a number + unit — pens state strength per dose, per ml, or per cartridge. NULL when unknown. 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 - manufacturer - doseStrength - 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. MedicationSupplySummary: type: object properties: unitsRemaining: type: number description: Pooled units across available (ACTIVE / IN_USE, units left) containers. Floored at 0 — a corrupt / legacy negative row can never surface a negative Bestand. unitsTotal: type: number description: Pooled capacity across the same available containers. dosesRemaining: type: number description: "Dose-derived headline: `floor(unitsRemaining / unitsPerDose)` (whole doses; a partial dose is not a dose)." dosesTotal: type: number description: "Dose-derived capacity: `floor(unitsTotal / unitsPerDose)`." expiredUnits: type: number description: Units still sitting in EXPIRED containers — visible to the user as a muted suffix, never folded into the available headline or the runway estimate. required: - unitsRemaining - unitsTotal - dosesRemaining - dosesTotal - expiredUnits additionalProperties: false description: Server-authoritative supply summary for a medication's containers. Computed from the same availability predicate the medications-list payload and the GLP-1 endpoint use, so every surface agrees on what 'remaining' means. 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: anyOf: - type: number - type: "null" description: 0-100, taken / (taken + missed). Skipped doses are excluded from the denominator — a deliberate decision, not a compliance failure. NULL when no dose was expected in the window (brand-new medication, paused). currentStreak: type: integer minimum: 0 maximum: 9007199254740991 description: Consecutive days ending at `asOf` where every expected dose was taken or skipped. Days with no expected dose advance the streak; missed days break it. longestStreak: type: integer minimum: 0 maximum: 9007199254740991 description: Longest all-taken-or-skipped run anywhere in the window. missedLast30: type: integer minimum: 0 maximum: 9007199254740991 description: Count of missed doses inside the window. windowDays: type: integer minimum: 0 maximum: 9007199254740991 description: Window size used — mirrors the input for the chart legend. required: - adherenceRate - currentStreak - longestStreak - missedLast30 - windowDays additionalProperties: false description: 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. GetMedicationEfficacyResponse: type: object properties: data: type: object properties: medicationId: type: string medicationName: type: string eligible: type: boolean reason: type: string enum: - one_shot - no_target startsOn: anyOf: - type: string - type: "null" resolution: type: object properties: tier: type: string enum: - override - atc - name - none cls: anyOf: - type: string - type: "null" required: - tier - cls additionalProperties: false windowDays: type: integer minimum: -9007199254740991 maximum: 9007199254740991 minWeeksPerSide: type: integer minimum: -9007199254740991 maximum: 9007199254740991 markers: type: object properties: start: anyOf: - type: string - type: "null" startSource: anyOf: - type: string enum: - startsOn - firstReading - type: "null" doseChanges: type: array items: type: object properties: at: type: string label: type: string required: - at - label additionalProperties: false pauses: type: array items: type: object properties: from: type: string to: anyOf: - type: string - type: "null" required: - from - to additionalProperties: false required: - start - startSource - doseChanges - pauses additionalProperties: false targets: type: array items: type: object properties: kind: type: string enum: - metric - lab key: type: string label: type: string unit: anyOf: - type: string - type: "null" primary: type: boolean referenceBand: anyOf: - type: object properties: low: type: number high: type: number required: - low - high additionalProperties: false - type: "null" series: type: array items: type: object properties: t: type: string value: type: number status: type: string enum: - in-range - below - above - unknown required: - t - value additionalProperties: false beforeAfter: type: object properties: present: type: boolean reason: type: string enum: - insufficient_before - insufficient_after - no_start - no_data before: type: object properties: mean: type: number count: type: integer minimum: -9007199254740991 maximum: 9007199254740991 from: type: string to: type: string required: - mean - count - from - to additionalProperties: false after: type: object properties: mean: type: number count: type: integer minimum: -9007199254740991 maximum: 9007199254740991 from: type: string to: type: string required: - mean - count - from - to additionalProperties: false delta: anyOf: - type: object properties: mean: type: number pct: anyOf: - type: number - type: "null" required: - mean - pct additionalProperties: false - type: "null" required: - present additionalProperties: false levelShift: anyOf: - type: object properties: present: type: boolean at: type: string nearStart: type: boolean required: - present additionalProperties: false - type: "null" required: - kind - key - label - unit - primary - referenceBand - series - beforeAfter - levelShift additionalProperties: false adherence: type: array items: type: object properties: date: type: string rate: type: number taken: type: integer minimum: -9007199254740991 maximum: 9007199254740991 missed: type: integer minimum: -9007199254740991 maximum: 9007199254740991 required: - date - rate - taken - missed additionalProperties: false overrideOptions: type: object properties: metrics: type: array items: type: object properties: key: type: string label: type: string required: - key - label additionalProperties: false biomarkers: type: array items: type: object properties: id: type: string label: type: string unit: type: string required: - id - label - unit additionalProperties: false required: - metrics - biomarkers additionalProperties: false required: - medicationId - medicationName - eligible - startsOn - resolution - windowDays - minWeeksPerSide - markers - targets - adherence - overrideOptions additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false SetMedicationEfficacyTargetResponse: type: object properties: data: type: object properties: ok: type: boolean cleared: type: boolean additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false GetDoseHistoryResponse: type: object properties: data: $ref: "#/components/schemas/DoseHistoryResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DoseHistoryResponse: type: object properties: from: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ to: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ family: type: string enum: - daily - weekly - one_shot - none description: Cadence family the window's slots were minted under. hasExpectedSlots: type: boolean rows: type: array items: $ref: "#/components/schemas/DoseHistoryRow" required: - from - to - family - hasExpectedSlots - rows additionalProperties: false description: "Per-slot dose ledger over [from, to]: every expected slot with a status plus every off-schedule intake tagged ad-hoc. Built from the same band minter + `reconstructDoseHistory` the compliance % consumes, so the history view and the rate never contradict each other." DoseHistoryRow: type: object properties: kind: type: string enum: - slot - ad_hoc description: "`slot` — a scheduled dose window; `ad_hoc` — a standalone off-schedule intake." 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: The slot anchor instant, or the ad-hoc take's own time. timeOfDay: anyOf: - type: string - type: "null" description: The slot's `HH:mm` label, or null for an ad-hoc row. status: type: string enum: - taken_on_time - taken_late - skipped - missed - upcoming - ad_hoc pinned: description: Present and true when the row is served by a deliberate user pin ('zugeordnet'). type: boolean nearestSlot: description: "Due-context for an ad-hoc take: the nearest slot it could belong to (preferring an unserved one). `filled` false means the slot can still be offered for pinning." type: object properties: 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)))$ timeOfDay: type: string filled: type: boolean required: - at - timeOfDay - filled additionalProperties: false intake: anyOf: - type: object properties: id: anyOf: - type: string - type: "null" 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 autoMissed: type: boolean doseTaken: anyOf: - type: string - type: "null" description: Per-intake dose override; null = configured dose. source: anyOf: - type: string enum: - WEB - API - REMINDER - IMPORT - APPLE_HEALTH - type: "null" description: "v1.32.8 (iOS #64) — how the dose was recorded: `WEB` (browser), `API` (Bearer / native app), `REMINDER` (the medication reminder worker), `IMPORT` (CSV importer), `APPLE_HEALTH` (the HealthKit dose-event mirror). Null on legacy rows written before the column carried a value. Derived server-side from the write transport, never client-asserted." required: - id - scheduledFor - takenAt - skipped - autoMissed - doseTaken - source additionalProperties: false - type: "null" description: The intake attributed to this row, if any. required: - kind - at - timeOfDay - status - intake additionalProperties: false 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 MedicationDoseHistoryImportDryRunEnvelope: type: object properties: data: $ref: "#/components/schemas/MedicationDoseHistoryImportResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MedicationDoseHistoryImportResponse: type: object properties: dryRun: type: boolean jobId: anyOf: - type: string - type: "null" description: Null on a dry run (nothing was queued). status: description: Present only on a real (non-dry-run) submission. type: string const: queued statusUrl: anyOf: - type: string - type: "null" description: Relative poll path (`/api/medications/intake/dose-history-import/{jobId}/status`). Null on a dry run. file: $ref: "#/components/schemas/MedicationDoseHistoryImportFile" required: - dryRun - jobId - statusUrl - file additionalProperties: false description: The file-level verdict, plus the queued job pointer on a real submission. A dry run (`?dryRun=1`) returns the same `file` verdict with `jobId`/`statusUrl` null and no `status` field — parses, matches, and reports without writing anything. MedicationDoseHistoryImportFile: type: object properties: rowsRead: type: integer minimum: 0 maximum: 9007199254740991 description: Data rows read from the file, refusals included. queued: type: integer minimum: 0 maximum: 9007199254740991 description: Rows that parsed and matched a medication. refused: type: integer minimum: 0 maximum: 9007199254740991 description: Total rows that did not queue, across every reason. refusedByReason: type: array items: $ref: "#/components/schemas/MedicationImportSkipGroup" description: One entry per reason that actually occurred, count descending then reason. unmatchedMedications: maxItems: 50 type: array items: type: string description: Medication names the file used that match nothing on the record, deduplicated, first-seen order, capped at 50. ambiguousMedications: maxItems: 50 type: array items: type: string description: Names that match more than one medication, so no single one can be meant. Capped at 50. mirroredMedications: maxItems: 50 type: array items: type: string description: Names that match only a medication mirrored from an external list. Capped at 50. unknownColumns: maxItems: 50 type: array items: type: string description: Header cells the importer has no verdict for. Capped at 50. codingsNotRead: type: integer minimum: 0 maximum: 9007199254740991 description: Rows whose `Codings` cell held something left unread. fromArchivedMedications: type: integer minimum: 0 maximum: 9007199254740991 description: Rows the file marked as belonging to a medication archived in the source. required: - rowsRead - queued - refused - refusedByReason - unmatchedMedications - ambiguousMedications - mirroredMedications - unknownColumns - codingsNotRead - fromArchivedMedications additionalProperties: false MedicationImportSkipGroup: type: object properties: reason: $ref: "#/components/schemas/MedicationImportSkipReason" count: type: integer minimum: 1 maximum: 9007199254740991 required: - reason - count additionalProperties: false MedicationImportSkipReason: type: string enum: - duplicate_in_file - already_recorded - medication_not_found - medication_ambiguous - medication_is_mirrored - status_no_dose_information - status_reminder_event - status_notification_not_sent - status_unknown - missing_timestamp - missing_timezone_offset - unreadable_timestamp - implausible_timestamp - missing_medication - unreadable_dosage - unreadable_row description: "Why one source row did not become an intake row. Closed 16-value set: duplicate_in_file, already_recorded, medication_not_found, medication_ambiguous, medication_is_mirrored, status_no_dose_information, status_reminder_event, status_notification_not_sent, status_unknown, missing_timestamp, missing_timezone_offset, unreadable_timestamp, implausible_timestamp, missing_medication, unreadable_dosage, unreadable_row." MedicationDoseHistoryImportQueuedEnvelope: type: object properties: data: $ref: "#/components/schemas/MedicationDoseHistoryImportResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MedicationIntakeImportJobStatusEnvelope: type: object properties: data: $ref: "#/components/schemas/MedicationIntakeImportJobStatusResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MedicationIntakeImportJobStatusResponse: type: object properties: jobId: type: string status: type: string enum: - queued - running - done - failed description: queued — admitted, not yet picked up. running — the worker is walking entries. done — finished (see `result` for the outcome, even a fully-refused run). failed — the worker could not complete the run (see `failureReason`). progress: $ref: "#/components/schemas/MedicationImportProgress" result: anyOf: - $ref: "#/components/schemas/MedicationImportResult" - type: "null" description: Set only once `status` is `done`; null before that. failureReason: anyOf: - type: string - type: "null" description: Set only when `status` is `failed`; null otherwise. 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)))$ startedAt: 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 worker picked the job up; null while still queued. completedAt: 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 job reached `done` or `failed`; null while active. required: - jobId - status - progress - result - failureReason - createdAt - startedAt - completedAt additionalProperties: false description: Poll target for a queued intake-import job — shared by the per-medication import and the account-wide dose-history import, so a client polls both through one shape. MedicationImportProgress: type: object properties: processed: type: integer minimum: 0 maximum: 9007199254740991 description: Entries the worker has walked so far, in file order. total: type: integer minimum: 0 maximum: 9007199254740991 description: Entries queued for this job. imported: type: integer minimum: 0 maximum: 9007199254740991 description: Entries that have reached the database so far. skippedByReason: type: object propertyNames: $ref: "#/components/schemas/MedicationImportSkipReason" additionalProperties: type: integer minimum: 1 maximum: 9007199254740991 description: Running skip tally by reason. A reason with no skips yet is absent from the object, never present as 0. skipDetails: description: Same bounded detail list as the finished `result`. type: array items: $ref: "#/components/schemas/MedicationImportSkipDetail" skippedDetailsOmitted: type: integer minimum: 0 maximum: 9007199254740991 touchedDays: type: array items: type: string description: Local calendar days (YYYY-MM-DD) touched so far, driving the chunked rollup recompute behind the write. rollupProcessed: type: integer minimum: 0 maximum: 9007199254740991 description: Of `touchedDays`, how many have had their rollup recomputed. required: - processed - total - imported - skippedByReason - touchedDays - rollupProcessed additionalProperties: false description: "In-flight job progress, written whole at admission and advanced in place by the worker. Present from the moment the job is created (queued), so `processed: 0, total: N` is the shape a client sees immediately after kickoff, not an empty object." MedicationImportSkipDetail: type: object properties: line: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: 1-based source-file row ordinal. reason: $ref: "#/components/schemas/MedicationImportSkipReason" required: - line - reason additionalProperties: false description: One refused source row, reduced to the ordinal + reason — never the row's own content (no medication name, timestamp, or dosage text). MedicationImportResult: type: object properties: imported: type: integer minimum: 0 maximum: 9007199254740991 skipped: type: integer minimum: 0 maximum: 9007199254740991 description: Total entries that did not become a row. skipReasons: type: array items: $ref: "#/components/schemas/MedicationImportSkipGroup" description: One entry per reason that actually occurred, count descending then reason — never the same sentence repeated per row. Empty when nothing was skipped. skipDetails: description: First bounded source-row details (capped at 200). Absent on a legacy job run before this field existed. type: array items: $ref: "#/components/schemas/MedicationImportSkipDetail" skippedDetailsOmitted: description: Count of additional skipped rows beyond the `skipDetails` cap, counted without retaining their contents. Absent when nothing was omitted. type: integer minimum: 0 maximum: 9007199254740991 required: - imported - skipped - skipReasons additionalProperties: false description: The finished job's outcome. Null while the job is still queued or running — `status` + `progress` are the fields to read meanwhile. 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 - OPENAI_COMPATIBLE - 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." compatBaseUrl: anyOf: - type: string - type: "null" description: "v1.33.1 (#470) — base URL of the OpenAI-compatible gateway (LiteLLM / OpenRouter / vLLM). Its own column: the OpenAI provider stays pinned to api.openai.com and never reads it." compatModel: anyOf: - type: string - type: "null" description: Model the gateway should route. Null falls back to `model`; with neither set the gateway entry does not resolve. hasCompatKey: type: boolean description: Whether a bearer is stored for the gateway. The bearer is optional — a gateway without auth needs none. responseTimeoutSeconds: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: v1.22 (#89) — per-user response timeout for AI generation, in seconds (10–600). Null = the built-in comprehensive-briefing default (~120 s). Mainly for slow local/self-hosted backends. required: - provider - model - baseUrl - aiAvailable - managedBy - hasAnthropicKey - anthropicKeyPreview - hasLocalKey - hasOpenaiKey - openaiKeyPreview - compatBaseUrl - compatModel - hasCompatKey - responseTimeoutSeconds 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: $ref: "#/components/schemas/CoachPrefsResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CoachPrefsResponse: 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 reminderSuggestions: type: object properties: enabled: default: true type: boolean stopped: default: false type: boolean dismissedCadences: default: [] maxItems: 32 type: array items: type: string maxLength: 64 lastSuggestedAt: default: null anyOf: - type: string maxLength: 40 - type: "null" required: - enabled - stopped - dismissedCadences - lastSuggestedAt additionalProperties: false updatedAt: description: "Optimistic-concurrency token: the stored row's `updatedAt` at read/write time. Echo it back as `baseUpdatedAt` on the next write. Opaque — only ever echo a server-returned value, never parse or synthesise it." type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: - tone - verbosity - excludeMetrics - showEvidenceByDefault - defaultWindow additionalProperties: false PutCoachPrefsResponse: type: object properties: data: $ref: "#/components/schemas/CoachPrefsResponse" 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 GetDiabetesResponse: type: object properties: data: $ref: "#/components/schemas/DiabetesFlagOutput" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false PatchDiabetesResponse: type: object properties: data: $ref: "#/components/schemas/DiabetesFlagOutput" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false HealthScoreConfigEnvelope: type: object properties: data: $ref: "#/components/schemas/HealthScoreConfigResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false HealthScoreConfigResponse: type: object properties: pillars: type: array items: $ref: "#/components/schemas/ScorePillarId" description: Registry-ordered pillars that count toward the score. Data availability narrows this further at score time, and that narrowing is not a choice — so this is the composition, not a promise that every pillar has a number today. excludedPillars: type: array items: $ref: "#/components/schemas/ScorePillarId" description: Registry-ordered pillars the person took out. hasSelection: type: boolean description: "True once a selection has been written. It says the person chose, not that their choice differs from the default: an account that opened the surface and kept every pillar has a selection. False means nobody ever chose, and such an account still inherits a later change of defaults." version: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Per-user recipe version, monotonic and incremented on every accepted write. 0 while no selection has been written. A score record names the version that produced it, so a comparison can see the recipe move. changedAt: 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 recipe last changed; null while it never has. updatedAt: description: "Optimistic-concurrency token: the stored row's `updatedAt` at read/write time. Echo it back as `baseUpdatedAt` on the next write. Opaque — only ever echo a server-returned value, never parse or synthesise it." type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: - pillars - excludedPillars - hasSelection - version - changedAt additionalProperties: false description: The resolved Health Score composition. `updatedAt` is ABSENT while the account has never written a selection — there is nothing to guard a write against, so the first save is the unconditional write. PatchHealthScoreConfigEnvelope: type: object properties: data: $ref: "#/components/schemas/HealthScoreConfigResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false GetModulesResponse: type: object properties: data: $ref: "#/components/schemas/ModulesResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false ModulesResponse: type: object properties: modules: $ref: "#/components/schemas/ModuleMap" updatedAt: description: "Optimistic-concurrency token: the stored row's `updatedAt` at read/write time. Echo it back as `baseUpdatedAt` on the next write. Opaque — only ever echo a server-returned value, never parse or synthesise it." type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: - modules additionalProperties: false ModuleMap: type: object properties: cycle: type: boolean description: Whether the "cycle" module is enabled for this account. mood: type: boolean description: Whether the "mood" module is enabled for this account. sleep: type: boolean description: Whether the "sleep" module is enabled for this account. glucose: type: boolean description: Whether the "glucose" module is enabled for this account. workouts: type: boolean description: Whether the "workouts" module is enabled for this account. recovery: type: boolean description: Whether the "recovery" module is enabled for this account. labs: type: boolean description: Whether the "labs" module is enabled for this account. illness: type: boolean description: Whether the "illness" module is enabled for this account. achievements: type: boolean description: Whether the "achievements" module is enabled for this account. coach: type: boolean description: Whether the "coach" module is enabled for this account. insights: type: boolean description: Whether the "insights" module is enabled for this account. medications: type: boolean description: Whether the "medications" module is enabled for this account. doctorReport: type: boolean description: Whether the "doctorReport" module is enabled for this account. environment: type: boolean description: Whether the "environment" module is enabled for this account. mcp: type: boolean description: Whether the "mcp" module is enabled for this account. inboundDocuments: type: boolean description: Whether the "inboundDocuments" module is enabled for this account. mentalHealth: type: boolean description: Whether the "mentalHealth" module is enabled for this account. nutrients: type: boolean description: Whether the "nutrients" module is enabled for this account. required: - cycle - mood - sleep - glucose - workouts - recovery - labs - illness - achievements - coach - insights - medications - doctorReport - environment - mcp - inboundDocuments - mentalHealth - nutrients additionalProperties: false description: Fully-resolved per-user module enable/disable map. Every toggleable module key is present. `false` means the surface should disappear end-to-end (nav, dashboard, insights, …). `cycle` mirrors `cycleTrackingEnabled`; `coach` mirrors the resolved per-user opt-out + operator master flag. PatchModulesResponse: type: object properties: data: $ref: "#/components/schemas/ModulesResponse" 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 - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA activeEnergy: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA walkingRunningDistance: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA flightsClimbed: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA sleep: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA weight: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA bloodPressure: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA pulse: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA bodyFat: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA bodyTemperature: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA spo2: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA hrv: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA restingHeartRate: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA vo2Max: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA skinTemperature: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA respiratoryRate: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA recovery: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA stress: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA sleepDebt: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA metricPriority: type: object properties: steps: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA activeEnergy: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA walkingRunningDistance: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA flightsClimbed: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA sleep: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA weight: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA bloodPressure: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA pulse: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA bodyFat: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA bodyTemperature: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA spo2: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA hrv: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA restingHeartRate: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA vo2Max: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA skinTemperature: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA respiratoryRate: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA recovery: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA stress: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA sleepDebt: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA 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 - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA activeEnergy: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA walkingRunningDistance: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA flightsClimbed: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA sleep: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA weight: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA bloodPressure: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA pulse: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA bodyFat: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA bodyTemperature: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA spo2: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA hrv: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA restingHeartRate: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA vo2Max: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA skinTemperature: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA respiratoryRate: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA recovery: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA stress: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA sleepDebt: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA metricPriority: type: object properties: steps: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA activeEnergy: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA walkingRunningDistance: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA flightsClimbed: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA sleep: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA weight: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA bloodPressure: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA pulse: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA bodyFat: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA bodyTemperature: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA spo2: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA hrv: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA restingHeartRate: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA vo2Max: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA skinTemperature: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA respiratoryRate: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA recovery: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA stress: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA sleepDebt: maxItems: 8 type: array items: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA 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 - OTHER - 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. dateFormat: type: string enum: - AUTO - DMY - MDY - YMD description: Date-order display preference. AUTO follows the locale convention, DMY pins day-month-year (dd.MM.yyyy), MDY pins month-day-year (MM/dd/yyyy), YMD pins ISO yyyy-MM-dd. 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. modules: $ref: "#/components/schemas/ModuleMap" required: - username - email - dateOfBirth - gender - heightCm - locale - timezone - timeFormat - dateFormat - moodReminderEnabled - fullName - insurerName - insurerIkNumber - insuranceNumber - modules 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. The `modules` map mirrors the /api/auth/me projection so the alias carries the same module flags. 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 - OTHER - 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. dateFormat: type: string enum: - AUTO - DMY - MDY - YMD description: Date-order display preference. AUTO follows the locale convention, DMY pins day-month-year (dd.MM.yyyy), MDY pins month-day-year (MM/dd/yyyy), YMD pins ISO yyyy-MM-dd. 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 - dateFormat - 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/NotificationPrefsResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false NotificationPrefsResponse: 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. reorderLeadDays: type: integer minimum: 0 maximum: 60 description: Reorder lead time in days (0–60) the low-stock alert assumes. The alert fires this lead plus one dose-interval before the supply runs out so a refill arrives before the last dose. Default 10; a per-medication reorderLeadDays overrides it. required: - clientManaged - deliveryDefault - lowStockRunwayDays - reorderLeadDays 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 updatedAt: description: "Optimistic-concurrency token: the stored row's `updatedAt` at read/write time. Echo it back as `baseUpdatedAt` on the next write. Opaque — only ever echo a server-returned value, never parse or synthesise it." type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: - medication - mood - cycle - coach additionalProperties: false PatchNotificationPrefsResponse: type: object properties: data: $ref: "#/components/schemas/NotificationPrefsResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false GetReportSelectionResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/GetReportSelectionResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false GetReportSelectionResponse: type: object properties: profile: anyOf: - $ref: "#/components/schemas/SavedReportProfile" - type: "null" description: The saved profile, or null when the account has never saved one, or when the stored blob no longer parses against the current leaf catalogue. Null is never resolved to a default scope server-side — the client applies its own named template and shows that as a visible choice. required: - profile additionalProperties: false PutReportSelectionResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/PutReportSelectionResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false PutReportSelectionResponse: type: object properties: profile: $ref: "#/components/schemas/SavedReportProfile" required: - profile additionalProperties: false CoachConversationsPageResponse: type: object properties: data: $ref: "#/components/schemas/CoachConversationsPage" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CoachConversationsPage: type: object properties: conversations: type: array items: $ref: "#/components/schemas/CoachConversation" nextCursor: anyOf: - type: string - type: "null" description: Id of the last conversation on this page; pass it back as `cursor` for the next page. Null at the end of the list. required: - conversations - nextCursor additionalProperties: false description: Cursor-paginated page of the caller's Coach conversations, most-recent activity first. CoachConversation: type: object properties: id: type: string title: type: string description: Title summarised from the first user message. 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)))$ description: Bumped on every appended message; the rail orders by this. messageCount: type: integer minimum: -9007199254740991 maximum: 9007199254740991 fenced: type: boolean description: "v1.29.x (S7) — the sticky fence flag. true = a FENCED thread: its turns route through the hardened fenced endpoint (no tools, no health snapshot), never the tool route. Permanent once true. false = a normal health thread." attachments: description: v1.29.x (S7) — the LIVE set of documents attached to this thread. Empty on a health thread (or a fenced thread whose attachments were all detached). type: array items: $ref: "#/components/schemas/CoachConversationAttachment" documentTitle: description: v1.29.x (S7) — the FIRST attachment's resolved title, kept for the rail's single-line badge. Null on a health thread or a fenced thread with no live attachment. anyOf: - type: string - type: "null" required: - id - title - createdAt - updatedAt - messageCount - fenced additionalProperties: false description: Lightweight conversation metadata for the history rail. No message bodies — the rail does not decrypt. CoachConversationAttachment: type: object properties: documentId: type: string title: anyOf: - type: string - type: "null" description: The attached document's resolved title (title, falling back to filename). Null when it has neither. Plaintext; no health values. required: - documentId - title additionalProperties: false CoachConversationDetailResponse: type: object properties: data: $ref: "#/components/schemas/CoachConversationDetail" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CoachConversationDetail: type: object properties: id: type: string title: 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)))$ 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)))$ messageCount: type: integer minimum: -9007199254740991 maximum: 9007199254740991 messages: type: array items: $ref: "#/components/schemas/CoachMessage" description: Every message in the conversation, decrypted, oldest-first. summary: description: Rolling summary of turns elided past the history window; null when none is on file. anyOf: - type: string - type: "null" fenced: type: boolean description: v1.29.x (S7) — the sticky fence flag; the client routes a fenced thread's turns through the hardened fenced endpoint, never the tool route. attachments: description: v1.29.x (S7) — the LIVE set of attached documents (join rows), attach-time order. type: array items: $ref: "#/components/schemas/CoachConversationAttachment" attachmentCount: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: v1.29.x (S7) — count of live attachment rows (equal to attachments.length). documentTitle: description: v1.29.x (S7) — the first attachment's resolved title for the rail badge. Null on a health thread. anyOf: - type: string - type: "null" required: - id - title - createdAt - updatedAt - messageCount - messages - fenced - attachmentCount additionalProperties: false description: Full conversation with every message decrypted server-side. The client renders the bodies directly; no decryption key is involved. CoachMessage: type: object properties: id: type: string role: type: string enum: - user - assistant content: type: string description: Decrypted message body — the server decrypts on read. 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)))$ metricSource: anyOf: - $ref: "#/components/schemas/CoachProvenance" - type: "null" description: Provenance envelope (assistant turns); null for user turns. providerType: anyOf: - type: string - type: "null" description: Provider that produced the reply (e.g. anthropic, openai, local, refusal); null for user turns. promptVersion: anyOf: - type: string - type: "null" description: Coach prompt version that produced the reply; null for user turns. tokensUsed: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: v1.18.9 — total tokens this assistant turn cost, persisted so the per-message token footer survives a reload. Null on user turns, refusals, and pre-feature rows. model: anyOf: - type: string - type: "null" description: v1.18.9 — the provider model that produced the reply (e.g. gpt-4o). Null when unknown (user turns, refusals, older rows). required: - id - role - content - createdAt - metricSource - providerType - promptVersion - tokensUsed - model additionalProperties: false description: One Coach chat message, decrypted server-side. Ordered oldest-first within a conversation. CoachProvenance: type: object properties: windows: type: array items: type: string enum: - last7days - last30days - last90days - lastYear - allTime description: Analysis windows the assistant drew on this turn. metrics: type: array items: type: string description: Stable metric-topic keys referenced (e.g. bp, weight, sleep, glucose). `general` is the empty-snapshot sentinel. The client translates these labels; the server never localises them. counts: description: Per-metric sample-count summary; absent on an empty snapshot. type: object propertyNames: type: string additionalProperties: type: integer minimum: -9007199254740991 maximum: 9007199254740991 keyValues: description: Load-bearing numbers the assistant surfaced, rendered in the collapsible evidence block. Hard-capped at 8 entries. type: array items: type: object properties: label: type: string value: type: string unit: type: string window: type: string required: - label - value additionalProperties: false toolCalls: description: "v1.20.0 — the retrieval-tool trace for this turn: which tools the Coach called and whether each found data. Metadata only (no values). Absent on the legacy snapshot path and on turns that called no tools." type: array items: type: object properties: name: type: string present: type: boolean required: - name - present additionalProperties: false unverifiedFigures: description: v1.32.14 — count of numeric tokens the grounding guard withheld from this reply (each rewritten to the editorial elision mark). Drives the quiet per-message notice. Count only, never the withheld values. Absent when the turn withheld nothing. type: integer exclusiveMinimum: 0 maximum: 9007199254740991 required: - windows - metrics additionalProperties: false description: Provenance envelope attached to an assistant message — labels and counts only, plus the optional evidence key-values. No raw timestamps. CoachConversationDeleted: type: object properties: data: type: object properties: deleted: type: boolean const: true description: Always true on success; a foreign / unknown id is a 404. required: - deleted additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CoachAttachmentsResponse: type: object properties: data: $ref: "#/components/schemas/CoachAttachments" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CoachAttachments: type: object properties: attachments: type: array items: $ref: "#/components/schemas/CoachConversationAttachment" description: The conversation's refreshed live attachment set. fenced: type: boolean description: The sticky fence flag — always true after an attach; unchanged (stays true) by a detach. required: - attachments - fenced additionalProperties: false CoachAttachmentsResponseDetach: type: object properties: data: $ref: "#/components/schemas/CoachAttachments" 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/InsightsLayoutResult" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false InsightsLayoutResult: 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 - health-status - breathing - labs-changes - ecg visible: type: boolean order: type: integer minimum: 0 maximum: 99 required: - id - visible - order additionalProperties: false tiles: minItems: 1 maxItems: 62 type: array items: type: object properties: id: type: string enum: - overview - blood-pressure - pulse - oxygen - body-temperature - respiratory-rate - pain - weight - bmi - body-water - bone-mass - fat-free-mass - fat-mass - muscle-mass - visceral-fat - lean-body-mass - waist - waist-to-height - 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 - grip-strength - 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 - nutrients - 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 updatedAt: description: "Optimistic-concurrency token: the stored row's `updatedAt` at read/write time. Echo it back as `baseUpdatedAt` on the next write. Opaque." type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: - version additionalProperties: false description: Resolved Insights layout plus the optimistic-concurrency `updatedAt` token. InsightsLayoutSaved: type: object properties: data: $ref: "#/components/schemas/InsightsLayoutResult" 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/InsightsLayoutResult" 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 CoachNudgeStatus: type: object properties: data: type: object properties: nudgedAt: 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: Timestamp of the newest Coach assistant message; null when none exists. unread: type: boolean description: True when that message is newer than the last time the caller opened the Coach. conversationId: anyOf: - type: string - type: "null" description: Conversation holding the newest assistant message; null when none exists. The client deep-links into it (`/coach?c=`) on the unread path. required: - nudgedAt - unread - conversationId additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CoachSeenResponse: type: object properties: data: type: object properties: seenAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: The server-minted open timestamp. required: - seenAt 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" aiIncludedSections: type: array items: type: string enum: - ABOUT_ME - CONDITIONS - ALLERGIES - COACH_FOCUS - FAMILY_HISTORY - SMOKING_STATUS - ALCOHOL_PATTERN - SHIFT_SCHEDULE 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 - aiIncludedSections - 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" aiIncludedSections: type: array items: type: string enum: - ABOUT_ME - CONDITIONS - ALLERGIES - COACH_FOCUS - FAMILY_HISTORY - SMOKING_STATUS - ALCOHOL_PATTERN - SHIFT_SCHEDULE 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 - aiIncludedSections - pendingQuestions - updatedAt - maxChars - fieldMaxChars additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false GetAnamnesisFactsResponse: type: object properties: data: type: object properties: current: type: object properties: SMOKING_STATUS: anyOf: - type: object properties: id: type: string kind: type: string enum: - SMOKING_STATUS - ALCOHOL_PATTERN - SHIFT_SCHEDULE value: anyOf: - type: string - type: "null" unreadable: type: boolean 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)))$ validUntil: 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" provenance: type: string enum: - USER_REPORTED - USER_CORRECTION supersededByRevisionId: 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 - kind - value - unreadable - validFrom - validUntil - provenance - supersededByRevisionId - createdAt additionalProperties: false - type: "null" ALCOHOL_PATTERN: anyOf: - type: object properties: id: type: string kind: type: string enum: - SMOKING_STATUS - ALCOHOL_PATTERN - SHIFT_SCHEDULE value: anyOf: - type: string - type: "null" unreadable: type: boolean 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)))$ validUntil: 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" provenance: type: string enum: - USER_REPORTED - USER_CORRECTION supersededByRevisionId: 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 - kind - value - unreadable - validFrom - validUntil - provenance - supersededByRevisionId - createdAt additionalProperties: false - type: "null" SHIFT_SCHEDULE: anyOf: - type: object properties: id: type: string kind: type: string enum: - SMOKING_STATUS - ALCOHOL_PATTERN - SHIFT_SCHEDULE value: anyOf: - type: string - type: "null" unreadable: type: boolean 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)))$ validUntil: 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" provenance: type: string enum: - USER_REPORTED - USER_CORRECTION supersededByRevisionId: 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 - kind - value - unreadable - validFrom - validUntil - provenance - supersededByRevisionId - createdAt additionalProperties: false - type: "null" required: - SMOKING_STATUS - ALCOHOL_PATTERN - SHIFT_SCHEDULE additionalProperties: false history: type: array items: type: object properties: id: type: string kind: type: string enum: - SMOKING_STATUS - ALCOHOL_PATTERN - SHIFT_SCHEDULE value: anyOf: - type: string - type: "null" unreadable: type: boolean 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)))$ validUntil: 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" provenance: type: string enum: - USER_REPORTED - USER_CORRECTION supersededByRevisionId: 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 - kind - value - unreadable - validFrom - validUntil - provenance - supersededByRevisionId - createdAt additionalProperties: false required: - current - history additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CreateAnamnesisFactResponse: type: object properties: data: type: object properties: id: type: string kind: type: string enum: - SMOKING_STATUS - ALCOHOL_PATTERN - SHIFT_SCHEDULE value: anyOf: - type: string - type: "null" unreadable: type: boolean 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)))$ validUntil: 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" provenance: type: string enum: - USER_REPORTED - USER_CORRECTION supersededByRevisionId: 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 - kind - value - unreadable - validFrom - validUntil - provenance - supersededByRevisionId - createdAt additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CorrectAnamnesisFactResponse: type: object properties: data: type: object properties: id: type: string kind: type: string enum: - SMOKING_STATUS - ALCOHOL_PATTERN - SHIFT_SCHEDULE value: anyOf: - type: string - type: "null" unreadable: type: boolean 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)))$ validUntil: 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" provenance: type: string enum: - USER_REPORTED - USER_CORRECTION supersededByRevisionId: 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 - kind - value - unreadable - validFrom - validUntil - provenance - supersededByRevisionId - createdAt additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false RemoveAnamnesisFactResponse: type: object properties: data: type: object properties: id: type: string kind: type: string enum: - SMOKING_STATUS - ALCOHOL_PATTERN - SHIFT_SCHEDULE removedAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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 - kind - removedAt 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 CoachPlansList: type: object properties: data: type: object properties: plans: type: array items: $ref: "#/components/schemas/CoachPlan" description: The caller's plans, newest first. Undecryptable rows are omitted from the list endpoint. No `status` filter returns the non-terminal set (proposed + active). required: - plans additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CoachPlan: type: object properties: id: type: string metric: type: string description: The metric this plan moves (e.g. WEIGHT, SLEEP). ifCue: anyOf: - type: string - type: "null" description: Decrypted if-cue (the trigger). Null when the row's key id is no longer in the map. thenAction: anyOf: - type: string - type: "null" description: Decrypted then-action. Null when the row's key id is no longer in the map. target: anyOf: - type: string - type: "null" description: Decrypted optional target; null when none / undecryptable. status: type: string enum: - proposed - active - met - abandoned - review_due - reviewed description: "App-side lifecycle: proposed | active | met | abandoned." reviewDate: 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: Optional check-in checkpoint; null when none. sourceConversationId: anyOf: - type: string - type: "null" description: Id of the conversation the proposal was extracted from; null for a plan without conversation provenance. Lets a chat surface show only the proposals born in the open thread. 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 - metric - ifCue - thenAction - target - status - reviewDate - sourceConversationId - createdAt - updatedAt additionalProperties: false description: One Coach goal / if-then plan, decrypted server-side. The free-text fields read null only when the encryption key id has rotated out of the map. CoachPlanUpdated: type: object properties: data: type: object properties: plan: description: The plan after the lifecycle update. $ref: "#/components/schemas/CoachPlan" required: - plan additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CoachPlanDeleted: type: object properties: data: type: object properties: deleted: type: boolean description: True when a plan 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 CoachReminderSuggestionResultOk: type: object properties: data: $ref: "#/components/schemas/CoachReminderSuggestionResult" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CoachReminderSuggestionResult: type: object properties: ok: type: boolean const: true action: type: string enum: - accept - dismiss - stop reminder: description: "On `accept` of a fresh suggestion: the created MeasurementReminder DTO. Null when the action was a prefs-only dismiss/stop, or when an accept hit the structural dedup (`duplicate: true`)." anyOf: - {} - type: "null" duplicate: description: True when an `accept` matched an already-live COACH reminder for the same metric (idempotent no-op). type: boolean required: - ok - action additionalProperties: false description: "Outcome of a Coach cadence-suggestion action. `accept` returns 201 with the created reminder (or 200 + `duplicate: true` when one already exists); `dismiss`/`stop` return 200." CoachReminderSuggestionResultCreated: type: object properties: data: $ref: "#/components/schemas/CoachReminderSuggestionResult" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CoachRemindersList: type: object properties: data: type: object properties: reminders: type: array items: $ref: "#/components/schemas/CoachReminder" description: The caller's reminders, soonest-due first. Undecryptable rows are omitted. No `status` filter returns the non-terminal set (proposed + active + due + surfaced). required: - reminders additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CoachReminder: type: object properties: id: type: string note: anyOf: - type: string - type: "null" description: Decrypted note (the user's own framing). Null when the row's key id is no longer in the map. metric: anyOf: - type: string - type: "null" triggerKind: type: string enum: - date - context dueAt: 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" contextCue: anyOf: - type: string - type: "null" status: type: string enum: - proposed - active - due - surfaced - done - dismissed source: type: string enum: - sentinel - extractor - manual - action 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 - note - metric - triggerKind - dueAt - contextCue - status - source - createdAt - updatedAt additionalProperties: false description: One Coach episodic reminder, decrypted server-side. The note reads null only when the encryption key id has rotated out of the map. CoachReminderCreated: type: object properties: data: type: object properties: reminder: $ref: "#/components/schemas/CoachReminder" required: - reminder additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CoachReminderUpdated: type: object properties: data: type: object properties: reminder: $ref: "#/components/schemas/CoachReminder" required: - reminder additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CoachReminderDeleted: type: object properties: data: type: object properties: deleted: type: boolean description: True when a reminder 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 CoachSuggestedActionResultCreated: type: object properties: data: $ref: "#/components/schemas/CoachSuggestedActionResult" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CoachSuggestedActionResult: type: object properties: ok: type: boolean const: true actionType: type: string enum: - checkup.create - reminder.note reminder: {} reminderId: type: string required: - ok - actionType additionalProperties: false description: Outcome of confirming a Coach action card. `checkup.create` returns the created MeasurementReminder DTO; `reminder.note` returns the created CoachReminder id. 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 invite universal-link (`/invite/`). On iOS it opens the app onto onboarding registration; in a browser it redirects to `/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 - OTHER - 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." targetBands: type: object properties: bpTargets: anyOf: - type: object properties: sysLow: type: number sysHigh: type: number diaLow: type: number diaHigh: type: number required: - sysLow - sysHigh - diaLow - diaHigh additionalProperties: false - type: "null" description: Personalised blood-pressure target numbers. Null when the profile has no date of birth. bpSysRange: anyOf: - type: object properties: greenMin: type: number greenMax: type: number orangeMin: type: number orangeMax: type: number required: - greenMin - greenMax - orangeMin - orangeMax additionalProperties: false - type: "null" description: Systolic traffic-light range. Null when the profile has no date of birth. bpDiaRange: anyOf: - type: object properties: greenMin: type: number greenMax: type: number orangeMin: type: number orangeMax: type: number required: - greenMin - greenMax - orangeMin - orangeMax additionalProperties: false - type: "null" description: Diastolic traffic-light range. Null when the profile has no date of birth. pulseDisplayRange: type: object properties: greenMin: type: number greenMax: type: number orangeMin: type: number orangeMax: type: number required: - greenMin - greenMax - orangeMin - orangeMax additionalProperties: false description: Resting-pulse display range. Always present — falls back to the AHA reference range with no date of birth. pulseBands: type: array items: type: object properties: min: type: number max: type: number color: type: string opacity: type: number strokeOpacity: type: number required: - min - max - color additionalProperties: false description: Resting-pulse chart bands. Always present — AHA fallback with no date of birth. bodyFatRange: type: object properties: min: type: number max: type: number required: - min - max additionalProperties: false description: Gender-aware body-fat target range. Always present. bodyFatBands: type: array items: type: object properties: min: type: number max: type: number color: type: string opacity: type: number strokeOpacity: type: number required: - min - max - color additionalProperties: false description: Body-fat chart bands. Always present. weightRange: anyOf: - type: object properties: greenMin: type: number greenMax: type: number orangeMin: type: number orangeMax: type: number required: - greenMin - greenMax - orangeMin - orangeMax additionalProperties: false - type: "null" description: "Weight traffic-light range: the user's own target band when set, else the height-derived WHO band, else null (no height and no target)." weightBands: anyOf: - type: array items: type: object properties: min: type: number max: type: number color: type: string opacity: type: number strokeOpacity: type: number required: - min - max - color additionalProperties: false - type: "null" description: Weight chart bands, from the same range. Null when `weightRange` is null. required: - bpTargets - bpSysRange - bpDiaRange - pulseDisplayRange - pulseBands - bodyFatRange - bodyFatBands - weightRange - weightBands additionalProperties: false description: Server-computed band / target math resolved from the user's profile facts, so the client reads these numbers instead of recomputing them. Every sub-field is null only when the driving profile fact is missing (no date of birth for blood pressure, no height and no weight target for weight). 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 sleepSourceDiscrepancy: description: Non-null when two writer buckets reported clearly different asleep totals for the latest night's main session (> 45 min apart and > 20% of the larger total). Observational only — the served summary stays the winning writer's totals; clients may show a discreet 'sources disagree' hint next to the sleep tile's headline. Null when the writers agree or the sleep module is off; optional for older cached snapshots. anyOf: - type: object properties: deltaMinutes: type: integer minimum: 0 maximum: 9007199254740991 sources: type: array items: type: object properties: source: type: string deviceType: anyOf: - type: string - type: "null" asleepMinutes: type: integer minimum: 0 maximum: 9007199254740991 required: - source - deviceType - asleepMinutes additionalProperties: false required: - deltaMinutes - sources additionalProperties: false - type: "null" 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" bpInTargetCount90: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" bpInTargetSpanDays90: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" glucoseByContext: type: object propertyNames: type: string additionalProperties: {} glucoseClinical: $ref: "#/components/schemas/GlucoseClinicalMetrics" required: - bpInTargetPct - bpInTargetPct7d - bpInTargetPct30d - bpInTargetPctAllTime - bpInTargetPctPriorMonth - bpInTargetPctPriorYear - bpInTargetCount90 - bpInTargetSpanDays90 - glucoseByContext - glucoseClinical 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" nextDueMedicationId: anyOf: - type: string - type: "null" dueCandidates: type: array items: type: object properties: medicationId: type: string medicationName: type: string dueAt: type: string overdue: type: boolean availableFrom: type: string required: - medicationId - medicationName - dueAt - overdue - availableFrom additionalProperties: false required: - activeCount - scheduledToday - takenToday - skippedToday - nextDueAt - nextDueOverdue - nextDueMedicationName - nextDueMedicationId additionalProperties: false description: "Today's medication block: active-medication count, today-window tally (scheduled / taken / skipped), every active medication's current display-due candidate, and legacy scalar fields projected from candidate zero. `overdue: true` marks an OPEN overdue slot (anchor passed, still inside its catch-up band); `availableFrom` is the canonical cadence- and dose-window-derived attribution-band start. `dueCandidates` is optional so older cached snapshots remain valid. Medication ids let consumers deep-link straight to the relevant card." healthScore: anyOf: - type: object properties: score: type: integer minimum: 0 maximum: 100 band: type: string enum: - green - yellow - red delta: anyOf: - type: number - type: "null" confidence: $ref: "#/components/schemas/DerivedConfidence" composition: type: array items: type: string enum: - BLOOD_PRESSURE - GLYCAEMIA - ACTIVITY - SLEEP - ADIPOSITY - WELLBEING - LIPIDS configured: description: "True when the account's own recipe narrows the score's composition below what its defaults would resolve to today. Server-resolved, so a client never interprets a configuration blob: an account that kept every pillar reads false, and so does one whose disabled modules alone narrow the set. The configuration itself is never on this wire. Optional so older cached snapshots without the field stay valid." type: boolean deltaReason: anyOf: - type: string enum: - algorithm_changed - config_changed - composition_changed - first_eligibility_window - below_noise_floor - no_previous_window - no_current_score - type: "null" scoreVersion: type: integer exclusiveMinimum: 0 maximum: 9007199254740991 bandSetter: anyOf: - type: string enum: - BLOOD_PRESSURE - GLYCAEMIA - ACTIVITY - SLEEP - ADIPOSITY - WELLBEING - LIPIDS - type: "null" restMode: anyOf: - type: object properties: active: type: boolean since: anyOf: - type: string - type: "null" episodeCount: type: integer minimum: -9007199254740991 maximum: 9007199254740991 required: - active - since - episodeCount additionalProperties: false - type: "null" tension: description: "Locale-agnostic tension verdict: present only when the readiness composite's contributors disagree. `positive`/`negative` carry readiness contributor keys the client maps to localised labels. Null on a coherent day or when a clinical red-flag suppresses it. Optional so older cached snapshots without the field stay valid." anyOf: - type: object properties: band: type: string enum: - green - yellow - red positive: type: array items: type: string enum: - rhr - hrv - sleep - respiratory - mood negative: type: array items: type: string enum: - rhr - hrv - sleep - respiratory - mood required: - band - positive - negative additionalProperties: false - type: "null" returnToBand: description: "Locale-agnostic return-to-baseline event: present only when a salient metric (resting heart rate, HRV, respiratory rate, or weight) has come back inside the user's own personal range after a genuine prior out-of-band run. At most one, the most salient. `metricType` is the measurement type the client maps to a localised metric name; `daysInside` is how long it has now sat back inside range. Null otherwise. Optional so older cached snapshots without the field stay valid." anyOf: - type: object properties: metricType: 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 - ANS_CHARGE - CARDIO_LOAD - SLEEP_SCORE - BODY_TEMPERATURE_DEVIATION - RESILIENCE - PHQ9_SCORE - GAD7_SCORE - GRIP_STRENGTH - PAIN_NRS - WAIST_CIRCUMFERENCE - WAIST_TO_HEIGHT - WHO5_SCORE - SCI_SCORE daysInside: type: integer minimum: -9007199254740991 maximum: 9007199254740991 required: - metricType - daysInside additionalProperties: false - type: "null" required: - score - band - delta additionalProperties: false - type: "null" description: Derived cardiometabolic reference score summary. Null until at least three eligible domains include a measured physiological domain. Additive metadata names confidence, ordered composition, the worst-pillar band setter, score version, and why a delta was suppressed. Rest Mode annotates but never changes the score. scoreRings: description: "User-selected hero score rings (max 3, `selectedScoreRings` on the dashboard layout), resolved server-side: READINESS / RECOVERY_SCORE / SLEEP_SCORE via the derived engines (module-gated like `/api/insights/derived`); MED_COMPLIANCE is TODAY's dose progress off the snapshot's medsToday tally — `score` is the rounded 0..100 progress, `doses` carries the taken/scheduled pair, the band is progress semantics (green once every scheduled dose is taken, yellow while doses remain, never red), and the ring is absent when no dose is scheduled today. Only rings with data appear, in selection order — a missing entry means no data or a disabled module, never zero. Clients render what arrives and never recompute." type: array items: type: object properties: id: type: string enum: - READINESS - RECOVERY_SCORE - SLEEP_SCORE - MED_COMPLIANCE score: type: integer minimum: -9007199254740991 maximum: 9007199254740991 band: type: string enum: - green - yellow - red doses: description: MED_COMPLIANCE only — today's dose tally behind the progress score, for a 'taken/scheduled' ring display (e.g. 1/3). Absent on the derived score rings. type: object properties: taken: type: integer minimum: -9007199254740991 maximum: 9007199254740991 scheduled: type: integer minimum: -9007199254740991 maximum: 9007199254740991 required: - taken - scheduled additionalProperties: false required: - id - score - band additionalProperties: false briefing: anyOf: - type: object propertyNames: type: string additionalProperties: {} - type: "null" briefingMemory: description: "The briefing recall + forward-look: `recall` is the prior period's narrative headline, `forward` points ahead to the most salient trend drift (or a calm 'holding steady' line). Both are already-localised prose the client renders verbatim. Null when no prior narrative is on file or no briefing is shown. Optional so older cached snapshots without the field stay valid." anyOf: - type: object properties: recall: type: string forward: type: string required: - recall - forward additionalProperties: false - 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 - targetBands - 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." GlucoseClinicalMetrics: type: object properties: stillLearning: type: boolean stillLearningReason: anyOf: - type: string - type: "null" windowDays: type: integer minimum: -9007199254740991 maximum: 9007199254740991 actualSpanDays: type: number readingCount: type: integer minimum: -9007199254740991 maximum: 9007199254740991 meanMgdl: anyOf: - type: number - type: "null" distribution: anyOf: - type: object properties: tir: type: number tbrLevel1: type: number tbrLevel2: type: number tarLevel1: type: number tarLevel2: type: number minutesEquivalent: type: object properties: tir: type: number tbrLevel1: type: number tbrLevel2: type: number tarLevel1: type: number tarLevel2: type: number required: - tir - tbrLevel1 - tbrLevel2 - tarLevel1 - tarLevel2 additionalProperties: false required: - tir - tbrLevel1 - tbrLevel2 - tarLevel1 - tarLevel2 - minutesEquivalent additionalProperties: false - type: "null" gmi: anyOf: - type: number - type: "null" estimatedA1c: anyOf: - type: number - type: "null" variability: anyOf: - type: object properties: sd: type: number cv: type: number unstable: type: boolean required: - sd - cv - unstable additionalProperties: false - type: "null" advanced: anyOf: - type: object properties: jIndex: anyOf: - type: number - type: "null" lbgi: type: number hbgi: type: number required: - jIndex - lbgi - hbgi additionalProperties: false - type: "null" isSpotEstimate: type: boolean required: - stillLearning - stillLearningReason - windowDays - actualSpanDays - readingCount - meanMgdl - distribution - gmi - estimatedA1c - variability - advanced - isSpotEstimate additionalProperties: false description: Server-authoritative glucose clinical panel over the trailing 30-day window. Figures from sparse spot data are a SPOT-READING ESTIMATE (a % of readings), not a CGM time-in-range AGP; `isSpotEstimate` is derived from reading density (true below ~hourly, false for a continuous CGM stream such as Nightscout) and `stillLearning` gates assertion when the window has too few readings or too short a span. `distribution` carries the Battelino 2019 TIR/TBR/TAR fractions (level-2 nested in level-1) plus minutes-of-a-day equivalents; `gmi` (Bergenstal 2018) + `estimatedA1c` (Nathan 2008 ADAG) derive from the mean; `variability` is SD + CV% with the Monnier 2017 ≥36% instability flag; `advanced` is the disclosure tier — J-index (Wojcicki 1995) + LBGI/HBGI (Kovatchev hypo/hyper risk). All blocks are null when there are no readings; `advanced.jIndex` is null for a single-reading window. 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 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. InsightsCardsEnvelope: type: object properties: data: $ref: "#/components/schemas/InsightsCardsResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false InsightsCardsResponse: type: array items: $ref: "#/components/schemas/InsightCard" InsightCard: type: object properties: id: type: string description: Stable per-card id (e.g. `alert-1`). title: type: string summary: type: string description: One-line alert message. body: anyOf: - type: string - type: "null" description: Longer narrative; null on the current rule-engine cards. severity: type: string enum: - alert - caution - info - good description: Mapped from the underlying alert level (danger→alert, warning→caution, success→good, else info). recommendations: type: array items: type: object properties: id: type: string label: type: string actionURL: anyOf: - type: string - type: "null" required: - id - label - actionURL additionalProperties: false description: Suggested follow-ups; empty on the current rule-engine cards. 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)))$ provider: type: string description: Lower-cased AI provider label for the account (e.g. `claude`). required: - id - title - summary - body - severity - recommendations - generatedAt - provider additionalProperties: false description: One iOS insight card, re-shaped from a server-side HealthAlert. Deterministic rule-engine output — no LLM call on this path. 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." BiomarkerAssessmentResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/BiomarkerAssessmentResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false BiomarkerAssessmentResponse: 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 marker has no numeric readings. 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 insufficient: description: True when the marker has no numeric readings; no assessment is generated (no LLM call). The card is not rendered. type: boolean required: - hasProvider - text - cached - updatedAt additionalProperties: false description: "Per-biomarker assessment envelope. 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; the assessment regenerates only when a new reading lands." 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 - HEALTH_SCORE - SAME_TIME_BASELINE 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 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. InsightsGlp1PlateauResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/InsightsGlp1PlateauResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false InsightsGlp1PlateauResponse: type: object properties: plateau: anyOf: - type: object properties: drug: type: string description: Display drug name ("Mounjaro"). doseValue: type: number description: Current dose value (e.g. 7.5). doseUnit: type: string description: Dose unit (e.g. "mg"). doseSince: type: string description: ISO date (YYYY-MM-DD) the current dose started. daysOnDose: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Days the user has been on the current dose. weightDeltaKg: type: number description: Weight delta in kg over the trailing window (negative = loss). readingsCount: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Number of weight readings considered. required: - drug - doseValue - doseUnit - doseSince - daysOnDose - weightDeltaKg - readingsCount additionalProperties: false - type: "null" description: Null when no plateau is detected (no active GLP-1 medication, < window days on the current dose, weight still dropping, or fewer than two readings). windowDays: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Trailing comparison window in days (currently 21). required: - plateau - windowDays additionalProperties: false description: "Deterministic weight-plateau detection for users on an active GLP-1 medication: stable dose for ≥ the window with no weight loss beyond the threshold. Association only — carries no verdict or advice." EcgListResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/EcgListResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false EcgListResponse: type: object properties: recordings: type: array items: $ref: "#/components/schemas/EcgRecordingListItem" description: The user's ECG recordings, newest first (capped at 200). hasRecordings: type: boolean description: False for an account with no recordings — the client un-mounts the whole ECG surface (data-availability floor). required: - recordings - hasRecordings additionalProperties: false description: Metadata-only ECG recording list. Never carries the waveform — the per-recording strip is fetched on demand from GET /api/insights/ecg/{id}. Read-only; no LLM call. Reflects only the device's own classification, never a HealthLog interpretation. EcgRecordingListItem: type: object properties: id: type: string description: Recording id (cuid). recordedAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: On-device recording time (the display timestamp). durationSeconds: anyOf: - type: number - type: "null" description: "`sampleCount / samplingFrequency` in seconds; null when the source omitted the sampling frequency." samplingFrequency: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Signal sampling rate in Hz (Withings ScanWatch = 300); 0 when the source omitted it. sampleCount: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Number of samples in the (encrypted) waveform array. averageHeartRate: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: Source-reported average heart rate (BPM) for the strip, when present. lead: anyOf: - type: string - type: "null" description: Recording lead label when the source reports it; null for the single-lead ScanWatch (Lead I). classification: anyOf: - type: string enum: - IRREGULAR - NOT_DETECTED - INCONCLUSIVE - type: "null" description: "The RECORDING DEVICE's own rhythm classification, surfaced verbatim: IRREGULAR (device flagged possible atrial fibrillation), NOT_DETECTED (algorithm ran, nothing flagged), INCONCLUSIVE (poor signal / out-of-range HR). Null when the source reported none. HealthLog never re-classifies an ECG — this is the device's certified on-device result, not a HealthLog verdict." source: type: string description: Measurement source that wrote the recording (e.g. WITHINGS). hasWaveform: type: boolean description: True when a waveform is stored (`sampleCount > 0`); false for a verdict-only fallback event with no signal to fetch. required: - id - recordedAt - durationSeconds - samplingFrequency - sampleCount - averageHeartRate - lead - classification - source - hasWaveform additionalProperties: false EcgIngestResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/EcgIngestResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false EcgIngestResponse: type: object properties: id: type: string description: The recording row's id (cuid). status: type: string enum: - inserted - updated - duplicate description: What the write did. `inserted` — a new recording landed (HTTP 201). `updated` — this `externalRecordingId` was already stored and the row was overwritten in place (HTTP 200); a retry is safe and creates nothing. `duplicate` — this exact recording is already stored under a DIFFERENT id, i.e. it arrived earlier through the `export.zip` importer (HTTP 200); nothing was written and `id` names the row that already holds it. A recording is the same recording when `(source, recordedAt, samplingFrequency)` match, which is enforced by a unique index rather than by convention. All three statuses mean the recording is stored and the client may advance its cursor. recordedAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: The `recordedAt` that was submitted, echoed back. sampleCount: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: "Server-derived: the number of samples received." durationSeconds: anyOf: - type: number - type: "null" description: "Server-derived: `sampleCount / samplingFrequency`." required: - id - status - recordedAt - sampleCount - durationSeconds additionalProperties: false description: The outcome of one ECG ingest. Reported honestly rather than optimistically — a re-post never claims to have inserted. EcgIngestCreatedEnvelope: type: object properties: data: $ref: "#/components/schemas/EcgIngestResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false EcgDetailResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/EcgDetailResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false EcgDetailResponse: type: object properties: recordedAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: On-device recording time (the display timestamp). durationSeconds: anyOf: - type: number - type: "null" description: Strip duration in seconds; null when the sampling frequency was 0. samplingFrequency: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Signal sampling rate in Hz (300 for a ScanWatch). averageHeartRate: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: Source-reported average heart rate (BPM) for the strip, when present. lead: anyOf: - type: string - type: "null" description: Recording lead label when reported; null for the single-lead ScanWatch (Lead I). classification: anyOf: - type: string enum: - IRREGULAR - NOT_DETECTED - INCONCLUSIVE - type: "null" description: "The RECORDING DEVICE's own rhythm classification, surfaced verbatim: IRREGULAR (device flagged possible atrial fibrillation), NOT_DETECTED (algorithm ran, nothing flagged), INCONCLUSIVE (poor signal / out-of-range HR). Null when the source reported none. HealthLog never re-classifies an ECG — this is the device's certified on-device result, not a HealthLog verdict." source: type: string description: Measurement source that wrote the recording. samples: type: array items: type: number description: Micro-volt waveform samples. Min/max-decimated to ~2500 points by default so R-wave peaks survive the fit-to-width compression; the raw array when `?full=1`. The x-position of each sample is implied by its index (an overview trace, not a calibrated-time axis) unless `decimated` is false. decimated: type: boolean description: True when `samples` was min/max-decimated for display; false when the raw array is returned (`?full=1`, or a strip already at or below the display budget). required: - recordedAt - durationSeconds - samplingFrequency - averageHeartRate - lead - classification - source - samples - decimated additionalProperties: false description: One ECG recording's decrypted waveform plus metadata and the DEVICE's verbatim classification. Ownership is narrowed in the query where — a foreign / unknown id 404s. The waveform is AES-256-GCM at rest, decrypted through the fail-closed codec. HealthLog does not interpret the trace, measure intervals, annotate beats, or emit a verdict of its own. no-store. RhythmEventsResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/RhythmEventsResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false RhythmEventsResponse: type: object properties: events: type: array items: $ref: "#/components/schemas/RhythmEventListItem" description: The user's device-flagged events, newest first (capped at 200). hasEvents: type: boolean description: False for an account with no event rows — the client un-mounts the whole surface (data-availability floor). required: - events - hasEvents additionalProperties: false description: Timeline of device-flagged EVENT rows (irregular-rhythm / high-HR / low-HR / walking-steadiness / breathing-disturbance) the user's wearable (Apple Watch / Withings ScanWatch) already produced and synced. AWARENESS / SCREENING of the DEVICE's own decision — HealthLog stores and reflects the classification result verbatim, never a raw waveform, and never re-classifies. Read-only; no LLM call. RhythmEventListItem: type: object properties: id: type: string description: Measurement row id (cuid). type: type: string enum: - IRREGULAR_RHYTHM_NOTIFICATION - HIGH_HEART_RATE_EVENT - LOW_HEART_RATE_EVENT - WALKING_STEADINESS_EVENT - BREATHING_DISTURBANCE_EVENT description: The HealthKit category the device flagged. IRREGULAR_RHYTHM_NOTIFICATION (`HKCategoryTypeIdentifierIrregularHeartRhythmEvent` + ScanWatch AFib screening), HIGH_HEART_RATE_EVENT / LOW_HEART_RATE_EVENT (a sustained heart rate above / below the user's configured threshold while apparently inactive), WALKING_STEADINESS_EVENT (`HKCategoryTypeIdentifierAppleWalkingSteadinessEvent`; the severity rides in `classification`), BREATHING_DISTURBANCE_EVENT (`HKCategoryTypeIdentifierSleepApneaEvent` + `AppleSleepingBreathingDisturbances`, an elevated breathing-disturbance / possible sleep-apnea screening signal). classification: anyOf: - type: string enum: - IRREGULAR - NOT_DETECTED - INCONCLUSIVE - LOW - VERY_LOW - FIRED - type: "null" description: The RECORDING DEVICE's own verdict, surfaced verbatim. IRREGULAR / NOT_DETECTED / INCONCLUSIVE are the ECG-style rhythm verdicts (device flagged a possible irregular rhythm / algorithm ran and found nothing / signal too poor to classify) and occur on an `IRREGULAR_RHYTHM_NOTIFICATION` row. LOW / VERY_LOW are walking-steadiness severities and occur on a `WALKING_STEADINESS_EVENT` row. FIRED is the neutral "event occurred" verdict with no severity gradient, and occurs on a `HIGH_HEART_RATE_EVENT` / `LOW_HEART_RATE_EVENT` / `BREATHING_DISTURBANCE_EVENT` row. Null when the source reported none. HealthLog never re-classifies — this is the device's certified on-device result. Distinct from `EcgRecordingListItem.classification` / `EcgDetailResponse.classification` above, which are scoped to the three ECG-only values. occurredAt: type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: Device-reported event time (`measuredAt`). source: type: string enum: - MANUAL - WITHINGS - IMPORT - APPLE_HEALTH - COMPUTED - WHOOP - FITBIT - NIGHTSCOUT - POLAR - OURA - TELEGRAM - MCP - GOOGLE_HEALTH - STRAVA deviceType: anyOf: - type: string - type: "null" description: Device class that reported the event (`watch | band | ring | phone | scale | other | unknown`); null when the source omitted it (treated as `unknown`). required: - id - type - classification - occurredAt - source - deviceType additionalProperties: false 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. emerging: description: v1.22 — rolling early-detection pass over the trailing window; emerging pairs not yet established retrospectively (no double-count). $ref: "#/components/schemas/EmergingCorrelationResult" labCorrelations: description: v1.22 — labs ↔ outcome associations (each draw vs the contemporaneous outcome window-mean), FDR-controlled; absent-degrading on sparse draws. $ref: "#/components/schemas/LabCorrelationResult" 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. behaviourLabel: description: Display label for a dynamic behaviour channel. type: string outcomeLabel: description: Display label for a dynamic outcome channel. type: string 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). shrunkR: type: number description: Sample-size-shrunk effect (r shrunk toward null by depth), used for ranking and the effect-size floor. Display still uses raw `r`. tier: type: string enum: - high - moderate - faint description: "Phrasing-confidence tier the narration honours: `faint` is hedged, `high` is stated plainly, `moderate` sits between. A below-floor pair never reaches this list." interpretation: type: string description: Conservative, descriptive interpretation — never causal. lagDays: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Lag in days applied (1). window: description: v1.22 — which window surfaced the pair. Absent on the 180-day scan (retrospective default); `recent` for an emerging early-detection pair. type: string enum: - retrospective - recent provisional: description: "v1.22 — true for an emerging recent-window pair: fewer days, hedged as provisional rather than established." type: boolean patternId: description: Account-scoped persisted identity for dismissal. type: string canonicalKey: description: Stable factor + outcome + lag identity. type: string dismissed: description: Server decision to suppress unchanged evidence. type: boolean required: - behaviour - outcome - n - r - pValue - qValue - shrunkR - tier - interpretation - lagDays additionalProperties: false EmergingCorrelationResult: type: object properties: emerging: type: array items: $ref: "#/components/schemas/DiscoveredCorrelation" description: Recent-window pairs NOT already established retrospectively — the emerging signals (provisional, hedged). windowDays: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Trailing window (days) the early pass scanned. minPairs: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Paired-day floor enforced for the early pass. fdrQ: type: number description: FDR target the early pass used (tighter than the main scan). pairsTested: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Pairs tested in the early window. required: - emerging - windowDays - minPairs - fdrQ - pairsTested additionalProperties: false LabCorrelationResult: type: object properties: discovered: type: array items: $ref: "#/components/schemas/DiscoveredLabCorrelation" description: Lab ↔ outcome associations surviving the per-pair floor + BH-FDR. pairsTested: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Lab × outcome pairs assessed. fdrQ: type: number description: The FDR target the pass used. minDraws: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Minimum paired-draw count enforced per pair. required: - discovered - pairsTested - fdrQ - minDraws additionalProperties: false DiscoveredLabCorrelation: type: object properties: lab: type: string description: "`LAB:` channel key (display strips the prefix)." outcome: type: string description: Outcome channel the marker tracks with (WEIGHT, BLOOD_GLUCOSE, BLOOD_PRESSURE_SYS). n: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Draws paired with a usable contemporaneous outcome window. r: type: number description: Pearson r over (draw value, contemporaneous outcome window-mean). 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). windowDays: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Trailing days each draw's outcome window spanned. interpretation: type: string description: Conservative, descriptive interpretation — never causal. required: - lab - outcome - n - r - pValue - qValue - windowDays - interpretation additionalProperties: false CorrelationPatternListResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/CorrelationPatternListResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CorrelationPatternListResponse: type: object properties: patterns: type: array items: $ref: "#/components/schemas/CorrelationPattern" required: - patterns additionalProperties: false CorrelationPattern: type: object properties: id: type: string canonicalKey: type: string family: type: string factorKey: type: string outcomeKey: type: string lagDays: type: integer minimum: -9007199254740991 maximum: 9007199254740991 sampleSize: type: integer minimum: -9007199254740991 maximum: 9007199254740991 effectSize: type: number pValue: type: number qValue: anyOf: - type: number - type: "null" evidenceHash: type: string lastComputedAt: type: string dismissedAt: anyOf: - type: string - type: "null" required: - id - canonicalKey - family - factorKey - outcomeKey - lagDays - sampleSize - effectSize - pValue - qValue - evidenceHash - lastComputedAt - dismissedAt additionalProperties: false UpdateCorrelationPatternResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/UpdateCorrelationPatternResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false UpdateCorrelationPatternResponse: type: object properties: id: type: string canonicalKey: type: string dismissed: type: boolean dismissedAt: anyOf: - type: string - type: "null" evidenceHash: type: string required: - id - canonicalKey - dismissed - dismissedAt - evidenceHash additionalProperties: false InsightsHealthStatusEnvelope: type: object properties: data: $ref: "#/components/schemas/InsightsHealthStatus" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false InsightsHealthStatus: type: object properties: present: type: boolean description: True when at least one deviation or shift is surfaced. deviations: type: array items: type: object properties: type: type: string description: MeasurementType token. value: type: number description: Today's value. center: type: number description: Personal band center (median). low: type: number high: type: number direction: type: string enum: - above - below required: - type - value - center - low - high - direction additionalProperties: false shifts: type: array items: type: object properties: metric: type: string description: MeasurementType token. breakDate: type: string description: YYYY-MM-DD of the new level's first day. beforeMean: type: number afterMean: type: number direction: type: string enum: - up - down required: - metric - breakDate - beforeMean - afterMean - direction additionalProperties: false generatedAt: type: string description: ISO-8601 instant the read was computed. required: - present - deviations - shifts - generatedAt additionalProperties: false description: Vitals drifting from the user's personal normal — band deviations plus dated, sustained level shifts. Awareness only, never a diagnosis. InsightsBreathingScreeningEnvelope: type: object properties: data: $ref: "#/components/schemas/InsightsBreathingScreening" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false InsightsBreathingScreening: type: object properties: present: type: boolean nights: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Nights with a per-night index reading. recentMeanIndex: anyOf: - type: number - type: "null" description: Mean of the index readings (lower-better); null when none. trend: anyOf: - type: string enum: - up - down - stable - type: "null" description: Recent index vs the prior window; null when too few nights. eventCount: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Device-flagged breathing-disturbance / apnea events. classification: anyOf: - type: string enum: - not-elevated - elevated - type: "null" description: The device's own classification; null when no data. generatedAt: type: string required: - present - nights - recentMeanIndex - trend - eventCount - classification - generatedAt additionalProperties: false description: Sleep-breathing-disturbance screening signal — a screening signal only, never a diagnosis. InsightsLabsChangesEnvelope: type: object properties: data: $ref: "#/components/schemas/InsightsLabsChanges" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false InsightsLabsChanges: type: object properties: present: type: boolean latestDate: anyOf: - type: string - type: "null" description: YYYY-MM-DD of the most-recent panel. previousDate: anyOf: - type: string - type: "null" description: YYYY-MM-DD of the prior panel. changes: type: array items: type: object properties: analyte: type: string unit: type: string latest: type: number previous: type: number delta: type: number description: Signed latest − previous. direction: type: string enum: - up - down - flat status: type: string enum: - in-range - below - above - unknown required: - analyte - unit - latest - previous - delta - direction - status additionalProperties: false generatedAt: type: string required: - present - latestDate - previousDate - changes - generatedAt additionalProperties: false description: Per-analyte change between the two most-recent numeric lab panels. Neutral framing, never a diagnosis. BulkMoodResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/BulkMoodResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false BulkMoodResponse: 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/BulkMoodEntryResult" required: - processed - inserted - duplicates - skipped - entries additionalProperties: false BulkMoodEntryResult: type: object properties: index: type: integer minimum: 0 maximum: 9007199254740991 status: type: string enum: - inserted - duplicate - skipped description: "`inserted`/`duplicate` — the row landed (advance the cursor). `skipped` — see `reason`." reason: type: string id: description: The upserted row id (absent on skips). type: string externalId: description: Echoed back when the entry carried one, for client mapping. type: string required: - index - status 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 updatedAt: description: "Optimistic-concurrency token: the stored row's `updatedAt` at read/write time. Echo it back as `baseUpdatedAt` on the next write. Opaque — only ever echo a server-returned value, never parse or synthesise it." type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: - groupOrder - placements additionalProperties: false description: "The stored layout merged over defaults: `groupOrder` fully resolved against the user's effective category set, `placements` as stored. `updatedAt` is the optimistic-concurrency token — echo it as `baseUpdatedAt` on the next PUT." 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. reorderLeadDays: type: integer minimum: 0 maximum: 60 description: v1.17.0 — per-user reorder lead default in days (0–60, default 10). The low-stock alert fires this lead plus one dose-interval before the supply runs out so a refill arrives before the last dose; a per-medication reorderLeadDays overrides it. Written via PATCH /api/auth/me/notification-prefs. required: - lateMinutes - missedMinutes - lowStockRunwayDays - reorderLeadDays 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. CsvImportResponseEnvelope: type: object properties: data: $ref: "#/components/schemas/CsvImportResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CsvImportResponse: type: object properties: inserted: type: integer minimum: -9007199254740991 maximum: 9007199254740991 updated: type: integer minimum: -9007199254740991 maximum: 9007199254740991 skipped: type: integer minimum: -9007199254740991 maximum: 9007199254740991 total: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Total data rows parsed (excludes header). dryRun: type: boolean rows: type: array items: $ref: "#/components/schemas/CsvImportRowResult" required: - inserted - updated - skipped - total - dryRun - rows additionalProperties: false description: Per-row import outcome. Mirrors the batch-route per-entry envelope. In `dryRun` mode no write runs and every valid row reports its projected `inserted` status. CsvImportRowResult: type: object properties: line: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: 1-based source line number (the header is line 1). status: type: string enum: - inserted - updated - skipped reason: description: Machine-readable reason for a non-inserted row (e.g. unknown_type, unknown_unit, missing_timezone_offset, value_out_of_range, implausible_timestamp, duplicate). type: string required: - line - status additionalProperties: false CsvImportTooLargeEnvelope: type: object properties: data: type: "null" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MeasurementReminderListEnvelope: type: object properties: data: type: array items: $ref: "#/components/schemas/MeasurementReminderDTO" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MeasurementReminderDTO: type: object properties: id: type: string label: type: string measurementType: anyOf: - $ref: "#/components/schemas/MeasurementReminderType" - type: "null" intervalDays: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" rrule: anyOf: - type: string - type: "null" anchorDate: 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" 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" origin: type: string enum: - VORSORGE - COACH notifyHour: type: integer minimum: -9007199254740991 maximum: 9007199254740991 location: anyOf: - type: string - 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" lastSatisfiedAt: 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" enabled: type: boolean 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 - label - measurementType - intervalDays - rrule - anchorDate - endsOn - origin - notifyHour - location - nextDueAt - lastSatisfiedAt - enabled - createdAt - updatedAt additionalProperties: false description: A Vorsorge reminder. nextDueAt is server-computed (server-authoritative). A free-text reminder carries measurementType=null and resolves only on a manual satisfy. origin distinguishes a user-created (VORSORGE) reminder from one minted by a Coach cadence suggestion (COACH); endsOn bounds a finite course window (null = open-ended). MeasurementReminderCreatedEnvelope: type: object properties: data: $ref: "#/components/schemas/MeasurementReminderDTO" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MeasurementReminderEnvelope: type: object properties: data: $ref: "#/components/schemas/MeasurementReminderDTO" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MeasurementReminderPatchEnvelope: type: object properties: data: $ref: "#/components/schemas/MeasurementReminderDTO" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MeasurementReminderDeleteEnvelope: 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 MeasurementReminderSatisfyEnvelope: type: object properties: data: $ref: "#/components/schemas/MeasurementReminderDTO" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MeasurementReminderCompleteEnvelope: type: object properties: data: $ref: "#/components/schemas/MeasurementReminderCompletion" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false MeasurementReminderCompletion: type: object properties: completed: type: boolean reminder: $ref: "#/components/schemas/MeasurementReminderDTO" required: - completed - reminder additionalProperties: false description: Result of an explicit reminder completion. completed=true when this call advanced lastSatisfiedAt; completed=false when an earlier satisfy or a matching reading had already fulfilled the current cycle (idempotent no-op). reminder is the canonical post-completion DTO. ListLabResultsEnvelope: type: object properties: data: $ref: "#/components/schemas/ListLabResultsResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false ListLabResultsResponse: type: object properties: results: type: array items: $ref: "#/components/schemas/LabResult" meta: type: object properties: total: type: number limit: type: number offset: type: number required: - total - limit - offset additionalProperties: false required: - results - meta additionalProperties: false LabResult: type: object properties: id: type: string biomarkerId: anyOf: - type: string - type: "null" panel: anyOf: - type: string - type: "null" analyte: type: string value: anyOf: - type: number - type: "null" valueText: anyOf: - type: string - type: "null" unit: type: string referenceLow: anyOf: - type: number - type: "null" referenceHigh: anyOf: - type: number - type: "null" takenAt: type: string source: type: string hasNote: type: boolean rangeStatus: $ref: "#/components/schemas/LabReferenceRangeStatus" createdAt: type: string updatedAt: type: string required: - id - biomarkerId - panel - analyte - value - valueText - unit - referenceLow - referenceHigh - takenAt - source - hasNote - rangeStatus - createdAt - updatedAt additionalProperties: false description: A stored lab result. Exactly one of `value` (numeric) or `valueText` (qualitative, e.g. "negativ") is non-null; `rangeStatus` is always `unknown` for a qualitative row (no number to classify). `biomarkerId` links the user-scoped catalog marker (null for legacy free-text rows); when linked, `analyte` / `unit` / `referenceLow` / `referenceHigh` are RESOLVED server-side from the Biomarker — the client renders them and never recomputes the range. The encrypted note is never echoed in list rows; `hasNote` flags its presence and the single-resource GET returns the decrypted `note`. LabReferenceRangeStatus: type: string enum: - in-range - below - above - unknown description: "Server-computed, NEUTRAL reference-range verdict. `unknown` when the lab reported no usable bounds. Inclusive bounds: a value on the limit reads in-range. The badge that renders this must stay calm and informative — not an alarming red." CreateLabResultResponse: type: object properties: data: $ref: "#/components/schemas/LabResult" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false GetLabResultResponse: type: object properties: data: $ref: "#/components/schemas/LabResultDetail" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false LabResultDetail: type: object properties: id: type: string biomarkerId: anyOf: - type: string - type: "null" panel: anyOf: - type: string - type: "null" analyte: type: string value: anyOf: - type: number - type: "null" valueText: anyOf: - type: string - type: "null" unit: type: string referenceLow: anyOf: - type: number - type: "null" referenceHigh: anyOf: - type: number - type: "null" takenAt: type: string source: type: string rangeStatus: $ref: "#/components/schemas/LabReferenceRangeStatus" createdAt: type: string updatedAt: type: string note: anyOf: - type: string - type: "null" required: - id - biomarkerId - panel - analyte - value - valueText - unit - referenceLow - referenceHigh - takenAt - source - rangeStatus - createdAt - updatedAt - note additionalProperties: false description: Single lab result including its decrypted free-text `note` (or null). UpdateLabResultResponse: type: object properties: data: $ref: "#/components/schemas/LabResult" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DeleteLabResultResponse: 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 RestoreLabResultsResponse: type: object properties: data: type: object properties: restored: type: number required: - restored additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false OcrCapabilityEnvelope: type: object properties: data: $ref: "#/components/schemas/OcrCapabilityResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false OcrCapabilityResponse: type: object properties: available: type: boolean mode: anyOf: - type: string enum: - vision - text - type: "null" reason: anyOf: - type: string enum: - no-provider - enable-local-ocr - type: "null" pdfSupported: type: boolean required: - available - mode - reason - pdfSupported additionalProperties: false description: Whether the caller's configured AI provider can ingest a lab report (drives the UI's scan affordance). `mode` is `vision` when the provider reads the image directly, `text` when the image is OCR'd in-browser and only the extracted text is sent (opt-in local OCR for text-only providers), or null when unavailable. `reason` explains an unavailable state; `pdfSupported` is true when the provider reads PDFs natively (Anthropic) or the server-side rasterizer can render the pages for any other vision provider. No provider call is made. OcrExtractEnvelope: type: object properties: data: $ref: "#/components/schemas/OcrExtractResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false OcrExtractResponse: type: object properties: reportDate: anyOf: - type: string - type: "null" providerType: type: string rows: type: array items: $ref: "#/components/schemas/OcrExtractedRow" required: - reportDate - providerType - rows additionalProperties: false description: The proposed rows for the human review screen. NEVER written to the database — extraction is read-only and the raw upload is held in memory only. OcrExtractedRow: type: object properties: analyte: type: string value: anyOf: - type: number - type: "null" valueText: anyOf: - type: string - type: "null" unit: anyOf: - type: string - type: "null" referenceLow: anyOf: - type: number - type: "null" referenceHigh: anyOf: - type: number - type: "null" takenAt: anyOf: - type: string - type: "null" confidence: type: object properties: analyte: type: number value: type: number unit: type: number range: type: number required: - analyte - value - unit - range additionalProperties: false biomarkerMatch: type: string enum: - new - existing duplicateOf: anyOf: - type: string - type: "null" required: - analyte - value - valueText - unit - referenceLow - referenceHigh - takenAt - confidence - biomarkerMatch - duplicateOf additionalProperties: false description: "One proposed reading transcribed from the upload. UNTRUSTED model output annotated server-side: `biomarkerMatch` flags whether the analyte links an existing catalog marker; `duplicateOf` is the id of a live reading this row likely duplicates (the review row defaults to unchecked when set); `confidence` is the model's per-field self-score (low fields are flagged for the human). Nothing is written until the commit route confirms." OcrCommitEnvelope: type: object properties: data: $ref: "#/components/schemas/OcrCommitResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false OcrCommitResponse: type: object properties: inserted: type: array items: $ref: "#/components/schemas/OcrCommittedLabResult" skipped: type: array items: type: object properties: analyte: type: string reason: type: string const: duplicate required: - analyte - reason additionalProperties: false outcome: type: string enum: - empty - failed - partial - success description: How the commit reads, resolved from the counts rather than the status code. `success` only when every confirmed row was written; `partial` when some were skipped; `failed` when nothing was written and at least one row was skipped; `empty` when there was nothing to write. required: - inserted - skipped - outcome additionalProperties: false description: 'The rows written (`source: "OCR"`), those skipped as commit-time duplicates, and the resolved outcome.' OcrCommittedLabResult: type: object properties: id: type: string biomarkerId: anyOf: - type: string - type: "null" panel: anyOf: - type: string - type: "null" analyte: type: string value: anyOf: - type: number - type: "null" valueText: anyOf: - type: string - type: "null" unit: type: string referenceLow: anyOf: - type: number - type: "null" referenceHigh: anyOf: - type: number - type: "null" takenAt: type: string source: type: string hasNote: type: boolean rangeStatus: type: string enum: - in-range - below - above - unknown createdAt: type: string updatedAt: type: string required: - id - biomarkerId - panel - analyte - value - valueText - unit - referenceLow - referenceHigh - takenAt - source - hasNote - rangeStatus - createdAt - updatedAt additionalProperties: false ListBiomarkersEnvelope: type: object properties: data: $ref: "#/components/schemas/ListBiomarkersResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false ListBiomarkersResponse: type: object properties: biomarkers: type: array items: $ref: "#/components/schemas/Biomarker" required: - biomarkers additionalProperties: false Biomarker: type: object properties: id: type: string name: type: string unit: type: string lowerBound: anyOf: - type: number - type: "null" upperBound: anyOf: - type: number - type: "null" panel: anyOf: - type: string - type: "null" hasContext: type: boolean context: anyOf: - type: string - type: "null" hidden: type: boolean createdAt: type: string updatedAt: type: string required: - id - name - unit - lowerBound - upperBound - panel - hasContext - context - hidden - createdAt - updatedAt additionalProperties: false description: A user-scoped catalog marker. `context` is the decrypted per-marker note (or null); `hasContext` flags its presence. `hidden` drops the marker from the active catalog list and the lab-entry pickers without deleting it (its readings and unit/range survive). A `LabResult` linking this marker resolves its unit + reference bounds from here. CreateBiomarkerResponse: type: object properties: data: $ref: "#/components/schemas/Biomarker" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false GetBiomarkerResponse: type: object properties: data: $ref: "#/components/schemas/Biomarker" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false UpdateBiomarkerResponse: type: object properties: data: $ref: "#/components/schemas/Biomarker" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DeleteBiomarkerResponse: 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 ListIllnessEpisodesEnvelope: type: object properties: data: type: array items: $ref: "#/components/schemas/IllnessEpisode" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false IllnessEpisode: type: object properties: id: type: string label: type: string type: type: string enum: - INFECTION - ALLERGY - INJURY - MENTAL_HEALTH - AUTOIMMUNE - CHRONIC - OTHER lifecycle: type: string enum: - ACUTE - CHRONIC_ONGOING - RECURRING - FLARE onsetAt: type: string resolvedAt: anyOf: - type: string - type: "null" parentConditionId: anyOf: - type: string - type: "null" note: anyOf: - type: string - type: "null" createdAt: type: string updatedAt: type: string required: - id - label - type - lifecycle - onsetAt - resolvedAt - parentConditionId - note - createdAt - updatedAt additionalProperties: false description: A stored illness/condition episode. `note` is the decrypted free-text (or null on a key-rotation gap — fail-soft, never 500). `resolvedAt` is null while the episode is open. `parentConditionId` links a FLARE/RECURRING bout to its parent condition. CreateIllnessEpisodeEnvelope: type: object properties: data: $ref: "#/components/schemas/IllnessEpisode" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false GetIllnessEpisodeEnvelope: type: object properties: data: $ref: "#/components/schemas/IllnessEpisode" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false UpdateIllnessEpisodeEnvelope: type: object properties: data: $ref: "#/components/schemas/IllnessEpisode" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DeleteIllnessEpisodeEnvelope: 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 RestoreIllnessEpisodeEnvelope: type: object properties: data: $ref: "#/components/schemas/IllnessEpisode" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false ResolveIllnessEpisodeEnvelope: type: object properties: data: $ref: "#/components/schemas/IllnessEpisode" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false GetIllnessDayLogEnvelope: type: object properties: data: anyOf: - anyOf: - $ref: "#/components/schemas/IllnessDayLog" - type: "null" - $ref: "#/components/schemas/IllnessDayLogList" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false IllnessDayLog: type: object properties: id: type: string episodeId: type: string date: type: string functionalImpact: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" feverC: anyOf: - type: number - type: "null" symptoms: type: array items: $ref: "#/components/schemas/IllnessSymptom" note: anyOf: - type: string - type: "null" updatedAt: type: string required: - id - episodeId - date - functionalImpact - feverC - symptoms - note - updatedAt additionalProperties: false description: A stored day-log for an episode. `date` is the `YYYY-MM-DD` it covers. `functionalImpact` (0–3) and `feverC` are plaintext; `symptoms` flattens the link rows; `note` is the decrypted free-text (or null). IllnessSymptom: type: object properties: key: type: string severity: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" required: - key - severity additionalProperties: false description: "A symptom link on a day-log: the catalog `key` plus an optional 1–3 graded severity (`null` = a plain presence link; the link's presence already means 'present', so 0 is not a distinct state)." IllnessDayLogList: type: object properties: dayLogs: type: array items: $ref: "#/components/schemas/IllnessDayLog" meta: type: object properties: total: type: number limit: type: number offset: type: number required: - total - limit - offset additionalProperties: false required: - dayLogs - meta additionalProperties: false description: "The date-less day-log LIST (v1.18.3): the episode's day-logs newest-first (or oldest-first under `sortDir:'asc'`), with `meta.total` for paging the full history. Powers the detail timeline scroll + iOS healthlog-iOS#30." UpsertIllnessDayLogUpdatedEnvelope: type: object properties: data: $ref: "#/components/schemas/IllnessDayLog" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false UpsertIllnessDayLogCreatedEnvelope: type: object properties: data: $ref: "#/components/schemas/IllnessDayLog" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false IllnessCorrelationEnvelope: type: object properties: data: $ref: "#/components/schemas/IllnessCorrelationResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false IllnessCorrelationResponse: type: object properties: episodeId: type: string status: type: string enum: - ok - insufficient value: anyOf: - $ref: "#/components/schemas/IllnessCorrelationValue" - type: "null" coverage: type: object properties: requiredInputs: type: integer minimum: -9007199254740991 maximum: 9007199254740991 presentInputs: type: integer minimum: -9007199254740991 maximum: 9007199254740991 historyDays: type: integer minimum: -9007199254740991 maximum: 9007199254740991 missing: type: array items: type: string required: - requiredInputs - presentInputs - historyDays - missing additionalProperties: false confidence: anyOf: - type: object properties: score: type: number band: type: string required: - score - band additionalProperties: false - type: "null" provenance: type: object properties: inputs: type: array items: type: string source: type: string windowDays: type: integer minimum: -9007199254740991 maximum: 9007199254740991 computedAt: type: string required: - inputs - source - windowDays - computedAt additionalProperties: false reason: anyOf: - type: string - type: "null" required: - episodeId - status - value - coverage - confidence - provenance - reason additionalProperties: false description: Coverage-gated `Derived` wire shape for the per-episode correlation. `status:"insufficient"` carries coverage + a reason and a null `value` — the surface renders 'still learning', never a fabricated number. Server-authoritative; iOS pattern-matches `status`, never recomputes. IllnessCorrelationValue: type: object properties: episodeId: type: string preOnset: type: array items: $ref: "#/components/schemas/IllnessVitalDeviation" nadir: type: array items: $ref: "#/components/schemas/IllnessVitalDeviation" returns: type: array items: $ref: "#/components/schemas/IllnessVitalReturn" recoveryGapDays: anyOf: - type: number - type: "null" adverseCoverageDays: type: integer minimum: -9007199254740991 maximum: 9007199254740991 feltBetterDay: anyOf: - type: string - type: "null" gapDriverType: anyOf: - type: string - type: "null" redFlags: type: array items: $ref: "#/components/schemas/IllnessRedFlag" sleepContext: anyOf: - $ref: "#/components/schemas/IllnessSleepContext" - type: "null" required: - episodeId - preOnset - nadir - returns - recoveryGapDays - adverseCoverageDays - feltBetterDay - gapDriverType - redFlags - sleepContext additionalProperties: false description: "The retrospective correlation findings for one episode: pre-onset anomaly scan, nadir, per-vital physiological returns (incl. the user-logged symptom-burden track, `type:FUNCTIONAL_IMPACT`), the headline recovery-gap (median of per-track gaps), the driving metric (`gapDriverType`), any red flags, and an optional sleep-as-context observation (`sleepContext`, never in the gap)." IllnessVitalDeviation: type: object properties: type: type: string day: type: string value: type: number baselineCenter: type: number deviationSd: type: number direction: type: string enum: - above - below adverse: type: boolean required: - type - day - value - baselineCenter - deviationSd - direction - adverse additionalProperties: false description: "One vital's deviation finding on a day: signed deviation in robust-SD units from the user's OWN baseline (median ± MAD), the direction, and whether the move is illness-adverse for that metric." IllnessVitalReturn: type: object properties: type: type: string returnedDay: anyOf: - type: string - type: "null" gapDays: anyOf: - type: number - type: "null" adverse: type: boolean required: - type - returnedDay - gapDays - adverse additionalProperties: false description: "A physiological / symptom return: the first day the metric re-entered its band AND held, the signed gap (days) from the felt-better marker (positive = the body lagged the feeling), and whether the move was illness-adverse (only adverse returns name the driver). `type` is a MeasurementType or `FUNCTIONAL_IMPACT` for the user-logged symptom-burden track (functionalImpact, baseline = constant 0)." IllnessRedFlag: type: object properties: type: type: string reason: type: string enum: - sustained_low_spo2 - sustained_fever worstValue: type: number days: type: integer minimum: -9007199254740991 maximum: 9007199254740991 required: - type - reason - worstValue - days additionalProperties: false description: A retrospective red-flag escalation (sustained low SpO2 or sustained fever) against absolute clinical floors. Copy must escalate ('seek care if this recurs'), never reassure. IllnessSleepContext: type: object properties: baselineMeanMinutes: type: number episodeMeanMinutes: type: number deltaMinutes: type: number nightsCounted: type: integer minimum: -9007199254740991 maximum: 9007199254740991 required: - baselineMeanMinutes - episodeMeanMinutes - deltaMinutes - nightsCounted additionalProperties: false description: "A neutral sleep-as-context observation: the user's median asleep minutes over the well baseline vs the mean over the episode window, and their signed delta (positive = slept more than usual). A pure OBSERVATION surfaced alongside the gap — NEVER a recovery return and NEVER part of the headline recovery-gap. Withheld (the field is null) on thin sleep data or a sub-floor delta." IllnessInsightsEnvelope: type: object properties: data: $ref: "#/components/schemas/IllnessInsightsResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false IllnessInsightsResponse: type: object properties: windowDays: type: integer minimum: -9007199254740991 maximum: 9007199254740991 episodeCount: type: integer minimum: -9007199254740991 maximum: 9007199254740991 resolvedCount: type: integer minimum: -9007199254740991 maximum: 9007199254740991 typicalRecoveryGapDays: anyOf: - type: number - type: "null" gapSampleSize: type: integer minimum: -9007199254740991 maximum: 9007199254740991 byMonth: type: object propertyNames: type: string additionalProperties: type: integer minimum: -9007199254740991 maximum: 9007199254740991 byType: type: object propertyNames: type: string additionalProperties: type: integer minimum: -9007199254740991 maximum: 9007199254740991 gapDriverType: anyOf: - type: string - type: "null" required: - windowDays - episodeCount - resolvedCount - typicalRecoveryGapDays - gapSampleSize - byMonth - byType - gapDriverType additionalProperties: false description: "Cross-episode retrospective summary over the trailing window: episode + resolved counts, the typical (median) recovery gap (null below the min-sample floor — withholds a thin claim), a recurrence-by-month tally, a per-type breakdown, and the dominant driving vital (`gapDriverType`, null when no gap is surfaced). Retrospective only." ListMentalHealthAssessmentsEnvelope: type: object properties: data: $ref: "#/components/schemas/ListMentalHealthAssessmentsResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false ListMentalHealthAssessmentsResponse: type: object properties: assessments: type: array items: $ref: "#/components/schemas/MentalHealthAssessment" required: - assessments additionalProperties: false MentalHealthAssessment: type: object properties: id: type: string instrument: type: string enum: - PHQ9 - GAD7 - WHO5 - SCI locale: type: string version: type: string totalScore: type: number severityBand: type: string item9Flagged: type: boolean crisisShownAt: anyOf: - type: string - type: "null" takenAt: type: string createdAt: type: string required: - id - instrument - locale - version - totalScore - severityBand - item9Flagged - crisisShownAt - takenAt - createdAt additionalProperties: false description: One completed screener administration. `severityBand` is a descriptive label for the screen (never a diagnosis). `item9Flagged` is true when the PHQ-9 self-harm item was answered > 0 — the client MUST show crisis resources when it is. Raw per-item answers are intentionally absent. CreateMentalHealthAssessmentEnvelope: type: object properties: data: $ref: "#/components/schemas/CreateMentalHealthAssessmentResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false CreateMentalHealthAssessmentResponse: type: object properties: assessment: $ref: "#/components/schemas/MentalHealthAssessment" actionThreshold: type: number crisis: anyOf: - $ref: "#/components/schemas/CrisisResourceSet" - type: "null" required: - assessment - actionThreshold - crisis additionalProperties: false CrisisResourceSet: type: object properties: emergencyNumber: type: string resources: type: array items: $ref: "#/components/schemas/CrisisResource" required: - emergencyNumber - resources additionalProperties: false description: Locale-aware crisis-resource signposting, present only when item-9 is flagged. Names resolve via i18n on the client; contacts are literal. CrisisResource: type: object properties: id: type: string contacts: type: array items: type: string required: - id - contacts additionalProperties: false ListAllergiesEnvelope: type: object properties: data: type: array items: $ref: "#/components/schemas/Allergy" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false Allergy: type: object properties: id: type: string substance: type: string category: type: string enum: - FOOD - MEDICATION - ENVIRONMENT - BIOLOGIC - OTHER type: type: string enum: - ALLERGY - INTOLERANCE severity: anyOf: - type: string enum: - MILD - MODERATE - SEVERE - type: "null" status: type: string enum: - ACTIVE - INACTIVE - RESOLVED onsetAt: anyOf: - type: string - type: "null" reaction: anyOf: - type: string - type: "null" note: anyOf: - type: string - type: "null" createdAt: type: string updatedAt: type: string required: - id - substance - category - type - severity - status - onsetAt - reaction - note - createdAt - updatedAt additionalProperties: false description: A stored allergy/intolerance record. `reaction` + `note` are the decrypted free-text (or null on a key-rotation gap — fail-soft, never 500). `severity`/`onsetAt` are null when not assessed/known. CreateAllergyEnvelope: type: object properties: data: $ref: "#/components/schemas/Allergy" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false GetAllergyEnvelope: type: object properties: data: $ref: "#/components/schemas/Allergy" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false UpdateAllergyEnvelope: type: object properties: data: $ref: "#/components/schemas/Allergy" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DeleteAllergyEnvelope: 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 ListFamilyHistoryEnvelope: type: object properties: data: type: array items: $ref: "#/components/schemas/FamilyHistoryEntry" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false FamilyHistoryEntry: type: object properties: id: type: string relationship: type: string enum: - MOTHER - FATHER - SISTER - BROTHER - DAUGHTER - SON - GRANDMOTHER_MATERNAL - GRANDFATHER_MATERNAL - GRANDMOTHER_PATERNAL - GRANDFATHER_PATERNAL - AUNT - UNCLE - COUSIN - HALF_SIBLING - OTHER condition: type: string ageAtOnset: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" note: anyOf: - type: string - type: "null" createdAt: type: string updatedAt: type: string required: - id - relationship - condition - ageAtOnset - note - createdAt - updatedAt additionalProperties: false description: "A stored family-history entry: one condition for one relative. `note` is the decrypted free-text (or null on a key-rotation gap — fail-soft, never 500). `ageAtOnset` is null when unknown." CreateFamilyHistoryEnvelope: type: object properties: data: $ref: "#/components/schemas/FamilyHistoryEntry" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false GetFamilyHistoryEnvelope: type: object properties: data: $ref: "#/components/schemas/FamilyHistoryEntry" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false UpdateFamilyHistoryEnvelope: type: object properties: data: $ref: "#/components/schemas/FamilyHistoryEntry" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DeleteFamilyHistoryEnvelope: 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 InboundDocumentListEnvelope: type: object properties: data: $ref: "#/components/schemas/InboundDocumentList" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false InboundDocumentList: type: object properties: documents: type: array items: $ref: "#/components/schemas/InboundDocument" nextCursor: anyOf: - type: string - type: "null" required: - documents - nextCursor additionalProperties: false description: A page of documents plus an opaque `nextCursor` (the id to pass back as `cursor` for the next page; null when the list is exhausted). InboundDocument: type: object properties: id: type: string kind: type: string enum: - DOCTOR_REPORT - DISCHARGE_LETTER - LAB_RESULT - IMAGING - PRESCRIPTION - REFERRAL - INSURANCE - VACCINATION - OTHER title: anyOf: - type: string - type: "null" filename: anyOf: - type: string - type: "null" mimeType: type: string byteSize: type: number status: type: string enum: - STORED - EXTRACTING - EXTRACTED - FAILED - CONFIRMED - DISCARDED providerType: anyOf: - type: string - type: "null" reportDate: anyOf: - type: string - type: "null" documentDate: anyOf: - type: string - type: "null" errorReason: anyOf: - type: string - type: "null" factCount: type: number pendingCount: type: number conditionLinks: type: array items: $ref: "#/components/schemas/DocumentConditionLink" servingClass: type: string enum: - inline - attachment hasContentIndex: type: boolean contentIndexSource: anyOf: - type: string enum: - vision - text-ocr - local-pdf - local-ocr - type: "null" hasThumbnail: type: boolean createdAt: type: string updatedAt: type: string required: - id - kind - title - filename - mimeType - byteSize - status - providerType - reportDate - documentDate - errorReason - factCount - pendingCount - conditionLinks - servingClass - hasContentIndex - contentIndexSource - hasThumbnail - createdAt - updatedAt additionalProperties: false description: A stored document. The raw bytes are stored encrypted at rest and never returned; this is the metadata + staging summary. `status` is STORED for a freshly uploaded file (no extraction run). `title` is the user label (plaintext); `documentDate` is the user filing date; `reportDate` is the model-transcribed date (null until extraction runs). `servingClass` says how `/original` delivers the file — render inline (`inline`) or download-only (`attachment`); render/download by it, never by MIME guess. `hasContentIndex` is true when the document has a content-search index (auto-indexed on upload); `contentIndexSource` says how that index was produced — `vision` means an AI provider read the original, the other values are provider-free local extractions, and it is null when `hasContentIndex` is false. `hasThumbnail` is true when a preview thumbnail has been rendered in the background (fetch it from `/api/documents/inbound/{id}/thumbnail`); false means no preview yet or an unsupported type — show a placeholder. Treat an unknown `kind` value as OTHER when decoding. DocumentConditionLink: type: object properties: episodeId: type: string name: type: string required: - episodeId - name additionalProperties: false description: A link to one of the caller's illness/condition episodes. `name` is the episode's user-facing label. InboundDocumentEnvelope: type: object properties: data: $ref: "#/components/schemas/InboundDocument" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DocumentAiCapabilityEnvelope: type: object properties: data: $ref: "#/components/schemas/DocumentAiCapability" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DocumentAiCapability: type: object properties: available: type: boolean mode: anyOf: - type: string enum: - vision - text - type: "null" reason: anyOf: - type: string enum: - no-provider - enable-local-ocr - type: "null" pdfSupported: type: boolean egress: anyOf: - type: string enum: - local - external - type: "null" required: - available - mode - reason - pdfSupported - egress additionalProperties: false DocumentUsageEnvelope: type: object properties: data: $ref: "#/components/schemas/DocumentUsage" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DocumentUsage: type: object properties: usedBytes: type: number quotaBytes: type: number maxFileBytes: type: number acceptedExtensions: type: array items: type: string linkedEpisodes: type: array items: type: object properties: episodeId: type: string name: type: string required: - episodeId - name additionalProperties: false assistAvailable: type: boolean contentIndex: type: object properties: enabled: type: boolean indexedCount: type: number totalCount: type: number required: - enabled - indexedCount - totalCount additionalProperties: false required: - usedBytes - quotaBytes - maxFileBytes - acceptedExtensions - linkedEpisodes - assistAvailable - contentIndex additionalProperties: false DocumentBulkEnvelope: type: object properties: data: $ref: "#/components/schemas/DocumentBulkResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DocumentBulkResponse: type: object properties: results: type: array items: type: object properties: id: type: string ok: type: boolean error: anyOf: - type: string - type: "null" required: - id - ok - error additionalProperties: false required: - results additionalProperties: false InboundDocumentDetailEnvelope: type: object properties: data: $ref: "#/components/schemas/InboundDocumentDetail" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false InboundDocumentDetail: type: object properties: id: type: string kind: type: string enum: - DOCTOR_REPORT - DISCHARGE_LETTER - LAB_RESULT - IMAGING - PRESCRIPTION - REFERRAL - INSURANCE - VACCINATION - OTHER title: anyOf: - type: string - type: "null" filename: anyOf: - type: string - type: "null" mimeType: type: string byteSize: type: number status: type: string enum: - STORED - EXTRACTING - EXTRACTED - FAILED - CONFIRMED - DISCARDED providerType: anyOf: - type: string - type: "null" reportDate: anyOf: - type: string - type: "null" documentDate: anyOf: - type: string - type: "null" errorReason: anyOf: - type: string - type: "null" factCount: type: number pendingCount: type: number conditionLinks: type: array items: $ref: "#/components/schemas/DocumentConditionLink" servingClass: type: string enum: - inline - attachment hasContentIndex: type: boolean contentIndexSource: anyOf: - type: string enum: - vision - text-ocr - local-pdf - local-ocr - type: "null" hasThumbnail: type: boolean createdAt: type: string updatedAt: type: string facts: type: array items: $ref: "#/components/schemas/ExtractedFact" summary: anyOf: - type: string - type: "null" summaryGeneratedAt: anyOf: - type: string - type: "null" summaryState: type: string enum: - NONE - PENDING - READY - WITHHELD - UNAVAILABLE required: - id - kind - title - filename - mimeType - byteSize - status - providerType - reportDate - documentDate - errorReason - factCount - pendingCount - conditionLinks - servingClass - hasContentIndex - contentIndexSource - hasThumbnail - createdAt - updatedAt - facts - summary - summaryGeneratedAt - summaryState additionalProperties: false description: 'A stored document plus its staged facts. `summary` is a short (3-4 sentence) plain-language description of WHAT the document is, generated once and then served from storage; it is descriptive only, never a diagnosis. `summaryGeneratedAt` is when it was generated (null until then). `summaryState` says what became of it, because a null `summary` on its own is ambiguous: NONE = never attempted (the `documentsAutoAiRead` opt-in was off at upload, or the document predates it — no backfill reaches these), PENDING = a job is enqueued or running, READY = stored and returned in `summary`, WITHHELD = generated but blocked by the outbound safety screen and therefore never returned as text, UNAVAILABLE = attempted and could not produce one (no vision provider, spent budget, withdrawn consent, unreadable file, provider error). Only PENDING may be presented as "being generated"; WITHHELD and UNAVAILABLE are both re-attemptable via POST `/api/documents/inbound/{id}/summary`. Treat an unknown value as UNAVAILABLE when decoding.' ExtractedFact: type: object properties: id: type: string factType: type: string enum: - CONDITION - OBSERVATION - MEDICATION_STATEMENT status: type: string enum: - PENDING - APPROVED - REJECTED confidence: type: number needsReview: type: boolean data: type: object propertyNames: type: string additionalProperties: {} provenance: type: object properties: sourceText: type: string anchored: type: boolean sourceOffset: anyOf: - type: number - type: "null" page: anyOf: - type: number - type: "null" confidence: type: number required: - sourceText - anchored - sourceOffset - page - confidence additionalProperties: false committedRecordId: anyOf: - type: string - type: "null" committedRecordType: anyOf: - type: string - type: "null" required: - id - factType - status - confidence - needsReview - data - provenance - committedRecordId - committedRecordType additionalProperties: false description: One STATED fact transcribed from the document, FHIR-staged (Condition / Observation / MedicationStatement) with per-field provenance + a confidence gate. STATED status only — codes (SNOMED/ICD-10/LOINC/RxNorm/ATC) are present only when the document wrote them; no range-flag, no inferred links. `needsReview` is true when the fact scored below the confidence floor (it cannot be approved until edited). InboundDocumentUpdateEnvelope: type: object properties: data: $ref: "#/components/schemas/InboundDocumentDetail" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false InboundDiscardEnvelope: type: object properties: data: type: object properties: id: type: string discarded: type: boolean required: - id - discarded additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false InboundRestoreEnvelope: type: object properties: data: $ref: "#/components/schemas/InboundDocumentDetail" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false InboundExtractEnvelope: type: object properties: data: $ref: "#/components/schemas/InboundDocumentDetail" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DocumentSuggestEnvelope: type: object properties: data: $ref: "#/components/schemas/DocumentSuggestResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DocumentSuggestResponse: type: object properties: suggestions: type: object properties: title: anyOf: - type: string - type: "null" kind: anyOf: - type: string enum: - DOCTOR_REPORT - DISCHARGE_LETTER - LAB_RESULT - IMAGING - PRESCRIPTION - REFERRAL - INSURANCE - VACCINATION - OTHER - type: "null" documentDate: anyOf: - type: string - type: "null" required: - title - kind - documentDate additionalProperties: false required: - suggestions additionalProperties: false DocumentSummaryEnvelope: type: object properties: data: $ref: "#/components/schemas/DocumentSummaryResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DocumentSummaryResponse: type: object properties: summary: type: string text: type: string persistence: type: string enum: - stored - withheld - failed additionalProperties: false DocumentIndexEnvelope: type: object properties: data: $ref: "#/components/schemas/DocumentIndexResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DocumentIndexResponse: type: object properties: documentId: type: string indexed: type: boolean tokenCount: type: number required: - documentId - indexed - tokenCount additionalProperties: false DocumentChatHistoryEnvelope: type: object properties: data: $ref: "#/components/schemas/DocumentChatHistoryResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DocumentChatHistoryResponse: anyOf: - $ref: "#/components/schemas/DocumentChatDetail" - $ref: "#/components/schemas/DocumentChatList" DocumentChatDetail: type: object properties: id: type: string title: type: string createdAt: type: string updatedAt: type: string messageCount: type: number messages: type: array items: $ref: "#/components/schemas/DocumentChatMessage" summary: anyOf: - type: string - type: "null" required: - id - title - createdAt - updatedAt - messageCount - messages - summary additionalProperties: false DocumentChatMessage: type: object properties: id: type: string role: type: string enum: - user - assistant content: type: string createdAt: type: string providerType: anyOf: - type: string - type: "null" tokensUsed: anyOf: - type: number - type: "null" model: anyOf: - type: string - type: "null" required: - id - role - content - createdAt - providerType - tokensUsed - model additionalProperties: false DocumentChatList: type: object properties: conversations: type: array items: $ref: "#/components/schemas/DocumentChatConversation" nextCursor: anyOf: - type: string - type: "null" required: - conversations - nextCursor additionalProperties: false DocumentChatConversation: type: object properties: id: type: string title: type: string createdAt: type: string updatedAt: type: string messageCount: type: number required: - id - title - createdAt - updatedAt - messageCount additionalProperties: false DocumentReindexEnvelope: type: object properties: data: $ref: "#/components/schemas/DocumentReindexResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DocumentReindexResponse: type: object properties: enqueued: type: boolean required: - enqueued additionalProperties: false ExtractedFactEnvelope: type: object properties: data: $ref: "#/components/schemas/ExtractedFact" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false InboundConfirmEnvelope: type: object properties: data: $ref: "#/components/schemas/InboundConfirmResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false InboundConfirmResponse: type: object properties: approved: type: array items: type: object properties: factId: type: string recordType: type: string recordId: type: string required: - factId - recordType - recordId additionalProperties: false rejected: type: array items: type: string needsReview: type: array items: type: string failed: type: array items: type: object properties: factId: type: string reason: type: string required: - factId - reason additionalProperties: false required: - approved - rejected - needsReview - failed additionalProperties: false description: "The outcome per decision: `approved` (committed, with the new record ref), `rejected` (discarded), `needsReview` (refused — edit first, fail-closed), `failed` (a per-fact commit miss, e.g. a numeric observation with no unit)." ListCustomMetricsEnvelope: type: object properties: data: $ref: "#/components/schemas/ListCustomMetricsResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false ListCustomMetricsResponse: type: object properties: customMetrics: type: array items: $ref: "#/components/schemas/CustomMetric" required: - customMetrics additionalProperties: false CustomMetric: type: object properties: id: type: string name: type: string unit: type: string targetLow: anyOf: - type: number - type: "null" targetHigh: anyOf: - type: number - type: "null" decimals: anyOf: - type: number - type: "null" description: anyOf: - type: string - type: "null" correlationEnabled: type: boolean latest: anyOf: - type: object properties: value: type: number unit: type: string measuredAt: type: string required: - value - unit - measuredAt additionalProperties: false - type: "null" entryCount: type: number createdAt: type: string updatedAt: type: string required: - id - name - unit - targetLow - targetHigh - decimals - description - correlationEnabled - latest - entryCount - createdAt - updatedAt additionalProperties: false description: A user-defined custom metric. `latest` is the most recently logged value (or null); `entryCount` is the total logged values. The optional target window is the user's own good range, charted as a reference band. CreateCustomMetricResponse: type: object properties: data: $ref: "#/components/schemas/CustomMetric" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false GetCustomMetricResponse: type: object properties: data: $ref: "#/components/schemas/CustomMetric" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false UpdateCustomMetricResponse: type: object properties: data: $ref: "#/components/schemas/CustomMetric" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DeleteCustomMetricResponse: 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 ListCustomMetricEntriesEnvelope: type: object properties: data: $ref: "#/components/schemas/ListCustomMetricEntriesResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false ListCustomMetricEntriesResponse: type: object properties: entries: type: array items: $ref: "#/components/schemas/CustomMetricEntry" meta: type: object properties: total: type: number limit: type: number offset: type: number required: - total - limit - offset additionalProperties: false required: - entries - meta additionalProperties: false CustomMetricEntry: type: object properties: id: type: string customMetricId: type: string value: type: number unit: type: string measuredAt: type: string note: anyOf: - type: string - type: "null" createdAt: type: string required: - id - customMetricId - value - unit - measuredAt - note - createdAt additionalProperties: false description: A single logged value for a custom metric. `unit` is a snapshot of the metric's unit at write time. CreateCustomMetricEntryResponse: type: object properties: data: $ref: "#/components/schemas/CustomMetricEntry" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false UpdateCustomMetricEntryResponse: type: object properties: data: $ref: "#/components/schemas/CustomMetricEntry" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DeleteCustomMetricEntryResponse: 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 DisclaimerAckEnvelope: type: object properties: data: $ref: "#/components/schemas/DisclaimerAckResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DisclaimerAckResponse: type: object properties: acknowledgedVersion: type: string description: The canonical disclaimer version the server stamped. required: - acknowledgedVersion additionalProperties: false description: The persisted disclaimer acknowledgment version. TourUpdateEnvelope: type: object properties: data: $ref: "#/components/schemas/TourUpdateResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false TourUpdateResponse: type: object properties: onboardingTourCompleted: type: boolean progress: anyOf: - $ref: "#/components/schemas/TourProgressOutput" - type: "null" required: - onboardingTourCompleted - progress additionalProperties: false description: The persisted completion flag and resume point after the write. NutrientBatchEnvelope: type: object properties: data: $ref: "#/components/schemas/NutrientBatchResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false NutrientBatchResponse: type: object properties: processed: type: integer minimum: 0 maximum: 9007199254740991 inserted: type: integer minimum: 0 maximum: 9007199254740991 updated: 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/NutrientEntryResult" required: - processed - inserted - updated - skipped - entries additionalProperties: false description: Always 200 when the envelope parses; per-entry failures surface as skipped entries, never a batch failure. NutrientEntryResult: type: object properties: index: type: integer minimum: 0 maximum: 9007199254740991 status: type: string enum: - inserted - updated - skipped reason: type: string required: - index - status additionalProperties: false description: "Per-entry ingest outcome. skipped reasons: unit_mismatch | value_out_of_range | day_invalid | upsert_failed. Log and drop a skipped entry — do not retry it." NutrientOverviewEnvelope: type: object properties: data: $ref: "#/components/schemas/NutrientIntakeOverview" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false NutrientIntakeOverview: type: object properties: windowDays: type: integer minimum: 1 maximum: 9007199254740991 nutrients: type: array items: type: object properties: nutrient: $ref: "#/components/schemas/NutrientCode" unit: type: string latestDay: type: string latestAmount: type: number daysWithData: type: integer minimum: 1 maximum: 9007199254740991 required: - nutrient - unit - latestDay - latestAmount - daysWithData additionalProperties: false lastAttempt: anyOf: - type: object properties: 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)))$ topReason: type: string required: - at - topReason additionalProperties: false - type: "null" description: Non-null only when `nutrients` is empty AND the most recent nutrient-batch call landed nothing — the most common skip reason from that call, so the client can say a sync arrived and why instead of a bare empty state. required: - windowDays - nutrients - lastAttempt additionalProperties: false description: "Per-nutrient window summary in catalog order: latest synced day + total, and the count of days carrying data inside the window. Nutrients without data in the window are omitted." NutrientDailySeriesEnvelope: type: object properties: data: $ref: "#/components/schemas/NutrientDailySeries" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false NutrientDailySeries: type: object properties: nutrient: $ref: "#/components/schemas/NutrientCode" unit: type: string windowDays: type: integer minimum: 1 maximum: 9007199254740991 days: type: array items: type: object properties: day: type: string amount: type: number required: - day - amount additionalProperties: false reference: anyOf: - type: object properties: kind: type: string enum: - PRI - AI - safeLevel direction: type: string enum: - target - upperGuidance value: type: number source: type: string required: - kind - direction - value - source additionalProperties: false - type: "null" required: - nutrient - unit - windowDays - days - reference additionalProperties: false description: Dense day-bucketed series (one entry per day in the window, 0 for a day with no data) summed across sources, plus the EFSA reference resolved against the caller's profile sex (null when sex is unknown on the profile). NutrientWaterWriteEnvelope: type: object properties: data: $ref: "#/components/schemas/NutrientWaterWriteResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false NutrientWaterWriteResponse: type: object properties: day: type: string nutrient: type: string const: water source: type: string const: MANUAL amount: type: number unit: type: string required: - day - nutrient - source - amount - unit additionalProperties: false description: The MANUAL water row after the add/set write. HealthKitConfigEnvelope: type: object properties: data: $ref: "#/components/schemas/HealthKitConfigResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false HealthKitConfigResponse: type: object properties: entries: type: array items: $ref: "#/components/schemas/HealthKitEntry" 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" description: When HealthKit last synced for this user; null when never (and always null on the PATCH echo). lastSyncTrigger: description: What the client declared triggered the most recent accepted batch. Null when the client sent no trigger — an older build, or a batch that predates the field. Present on the GET read; omitted from the PATCH echo. anyOf: - type: string enum: - foreground - background - push description: What triggered a HealthKit batch. - type: "null" lastBackgroundSyncAt: description: When a batch last arrived without the app being opened (a `background` or `push` trigger). Null when that has never happened. `lastSyncedAt` alone cannot separate a phone that delivers on its own from one that only delivers while the app is open; this field is what does. Present on the GET read; omitted from the PATCH echo. 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" syncHealth: description: Present on the GET read; omitted from the PATCH echo. $ref: "#/components/schemas/SyncHealth" metricFreshness: description: Per-metric-type freshness for the `APPLE_HEALTH` source. Present on the GET read; omitted from the PATCH echo. type: array items: $ref: "#/components/schemas/MetricFreshnessEntry" required: - entries - lastSyncedAt additionalProperties: false description: "The resolved HealthKit integration config: the default metric set merged with the user's stored per-metric overrides, plus the sync-health verdict and per-metric freshness." HealthKitEntry: type: object properties: id: type: string description: Stable metric key (e.g. `bodyMass`). kind: type: string description: HealthKit sample kind (e.g. `bloodPressure`). direction: type: string enum: - bidirectional - readOnly - writeOnly - disabled description: Per-metric sync direction. enabled: type: boolean required: - id - kind - direction - enabled additionalProperties: false description: One resolved HealthKit metric mapping (defaults merged with the user's stored overrides). SyncHealth: type: object properties: verdict: type: string enum: - fresh - stale - stalled - failing - reauth_required - parked - pending_first_sync - disconnected description: "Liveness verdict. For Apple Health: `fresh` when data arrived within the last 7 days, `stale` when it did not, `pending_first_sync` when none ever arrived." since: 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 instant that triggered the verdict; null when none did. required: - verdict - since additionalProperties: false description: Server-resolved sync-health verdict. The single source of liveness truth — `lastSyncedAt` alone cannot express 'this pipe stopped delivering'. MetricFreshnessEntry: type: object properties: type: type: string description: The measurement type (e.g. `RESPIRATORY_RATE`), or `WORKOUTS` for the workout leg. 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)))$ description: Newest recorded reading for this type from this source. stale: type: boolean description: This type has gone quiet while the source around it reads healthy — the dead-pipe signature (e.g. a single revoked HealthKit permission). Only ever true when the verdict is `fresh`. required: - type - lastSeenAt - stale additionalProperties: false description: Per-metric-type last-seen timestamp with the server-computed staleness flag. Only types that have actually delivered appear — absence is absence, never an invented row. HealthKitConfigPatchEnvelope: type: object properties: data: $ref: "#/components/schemas/HealthKitConfigResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false OuraSyncEnvelope: type: object properties: data: $ref: "#/components/schemas/ProviderSyncOutcome" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false ProviderSyncOutcome: type: object properties: imported: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: Rows that reached the database this run. failed: type: boolean description: True when any part of the run did not land — a row, a collection, a leg. outcome: type: string enum: - empty - failed - partial - success description: "`empty` — nothing new, nothing refused. `failed` — nothing written, something refused. `partial` — some written, some refused. `success` — everything written." required: - imported - failed - outcome additionalProperties: false description: The result of an on-demand provider sync trigger. No request body; the sync window is derived server-side from the newest row already held, never a client-supplied lookback. PolarSyncEnvelope: type: object properties: data: $ref: "#/components/schemas/ProviderSyncOutcome" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false StravaSyncEnvelope: type: object properties: data: $ref: "#/components/schemas/ProviderSyncOutcome" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false NightscoutSyncEnvelope: type: object properties: data: $ref: "#/components/schemas/ProviderSyncOutcome" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false GoogleHealthSyncStatusEnvelope: type: object properties: data: $ref: "#/components/schemas/GoogleHealthSyncStatusResponse" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false GoogleHealthSyncStatusResponse: anyOf: - type: object properties: runId: type: string description: Truncated to 128 chars. state: type: string enum: - in_progress - complete - partial - zero - truncated - failed - interrupted description: Falls back to `failed` when the stored value is not one of this closed set. startedAt: type: string description: ISO instant, or empty string when unset. updatedAt: type: string description: ISO instant, or empty string when unset. terminalAt: description: ISO instant the run reached a terminal state. Present only once the run has finished; absent while `state` is `in_progress`. type: string imported: type: integer minimum: 0 maximum: 9007199254740991 failed: type: boolean resources: maxItems: 16 type: array items: $ref: "#/components/schemas/GoogleHealthResourceOutcome" description: Per-collection outcome for this run, capped at 16 entries. required: - runId - state - startedAt - updatedAt - imported - failed - resources additionalProperties: false - type: "null" description: The caller's current/most-recent Google Health sync run, or null when none has ever run. `data` carries this shape directly (not nested under a named key). GoogleHealthResourceOutcome: type: object properties: resource: type: string description: Resource collection name, lower-cased and slug-sanitised (`[^a-z0-9-]` stripped), capped at 48 chars. `unknown` when the stored value was not a string. pages: type: integer minimum: 0 maximum: 9007199254740991 fetched: type: integer minimum: 0 maximum: 9007199254740991 mapped: type: integer minimum: 0 maximum: 9007199254740991 written: type: integer minimum: 0 maximum: 9007199254740991 status: type: string enum: - pending - complete - partial - empty - truncated - failed description: Falls back to `failed` when the stored value is not one of this closed set. durationMs: type: integer minimum: 0 maximum: 9007199254740991 truncated: type: boolean reasonCode: anyOf: - type: string enum: - collection_failed - token_failed - upsert_failed - rollup_failed - existing_page_limit - type: "null" description: Null when the stored value is not one of this closed set, or when the resource did not fail. required: - resource - pages - fetched - mapped - written - status - durationMs - truncated - reasonCode additionalProperties: false DailyDigestEnvelope: type: object properties: data: $ref: "#/components/schemas/DailyDigest" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DailyDigest: type: object properties: generatedAt: type: string description: ISO-8601 instant the digest was read. phase: type: string enum: - provisional - final description: Freshness lifecycle — 'final' once last night's sleep is in. sleepPending: type: boolean description: Sleep tracked but last night not yet recorded. score: anyOf: - type: object properties: value: type: number band: type: string delta: anyOf: - type: number - type: "null" configured: description: True when the account's own recipe narrows the score's composition below what its defaults would resolve to today. Resolved on the server and carried straight from the dashboard snapshot; the configuration itself never rides this wire. Optional so older cached digests without the field stay valid. type: boolean deltaReason: description: Why a delta was suppressed; null when it was not. anyOf: - type: string - type: "null" scoreVersion: description: Algorithm version that produced the value. type: integer exclusiveMinimum: 0 maximum: 9007199254740991 composition: description: Registry-ordered pillar ids the value was composed of. type: array items: type: string required: - value - band - delta additionalProperties: false - type: "null" description: Health score + band + week-over-week delta, plus whether the account authored the composition; null when none. topSignal: anyOf: - type: object properties: sourceMetric: type: string tone: type: string enum: - good - watch - info headline: type: string nudge: type: string delta: anyOf: - type: string - type: "null" required: - sourceMetric - tone - headline - nudge - delta additionalProperties: false - type: "null" description: Clinical-priority top signal, lifted from the cached briefing. briefingLead: anyOf: - type: string - type: "null" description: First sentence of the cached briefing paragraph. line: type: string description: "Push / lock-screen line: cached-AI lead with a deterministic floor." worthALook: type: array items: $ref: "#/components/schemas/DailyPriorityItem" description: Bounded 0–3 rail items, never padded. justIn: anyOf: - type: object properties: kind: type: string enum: - sleep_night - workout - weight - blood_pressure - labs_panel description: Closed arrival kind that landed. at: type: string description: ISO-8601 instant of the newest sample. NEVER pre-formatted server-side — the client formats it in its own locale and timezone. required: - kind - at additionalProperties: false - type: "null" description: The day's newest data arrival while it is still news (under three hours old), else null. Additive since v1.31.0. reactionLine: anyOf: - type: string - type: "null" description: One-sentence generated reaction to that arrival, standing for the rest of the local day. Null whenever no line was generated (no provider, no consent, budget exhausted, or generation failed) — consumers fall back to `briefingLead` / `line`. Additive since v1.31.0. required: - generatedAt - phase - sleepPending - score - topSignal - briefingLead - line - worthALook - justIn - reactionLine additionalProperties: false description: The one data spine of the daily-value system — composed from already-cached data, never a fresh AI call. Consumed by the Today surface, the daily push, and a future iOS widget. DailyPriorityItem: type: object properties: kind: type: string enum: - coach_checkin - dose_window - preventive_care - sync_issue - milestone - ecg_new_recording - tension_window - same_time_baseline description: Closed rail-item kind. itemKey: description: Stable dismiss identity, namespaced `:...`. Present only on the observational kinds (milestone / ecg_new_recording / tension_window) — absent on the actionable kinds, which are never dismissible. type: string title: type: string description: Localised headline (resolved server-side). body: description: Grounded one-liner, rendered as plain text. type: string status: description: Semantic status wash — meaning, not decoration. type: string enum: - success - warning - info - destructive actions: maxItems: 3 type: array items: type: object properties: labelKey: type: string description: i18n key, resolved client-side. intent: type: string description: Stable action token. href: description: Deep-link when navigational. type: string required: - labelKey - intent additionalProperties: false description: 1–3 one-tap actions. moduleKey: description: Provenance of the gate that admitted the item. type: string required: - kind - title - actions additionalProperties: false description: One 'worth a look' rail item. The single model every daily-value consumer renders through PriorityCard. DismissPriorityItemEnvelope: type: object properties: data: type: object properties: dismissed: type: boolean const: true required: - dismissed additionalProperties: false error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DashboardLayout: type: object properties: data: $ref: "#/components/schemas/DashboardLayoutResult" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DashboardLayoutResult: type: object properties: version: type: number const: 1 widgets: minItems: 1 maxItems: 50 type: array items: type: object properties: id: type: string enum: - weight - bp - pulse - bodyFat - mood - medications - sleep - steps - glucose - totalBodyWater - boneMass - muscleMass - bpInTarget - oxygenSaturation - hrv - respiratoryRate - achievements - vo2Max - recentWorkouts - cardioRecovery - sixMinuteWalk - stairAscentSpeed - stairDescentSpeed - breathingDisturbances - wristTemperature - falls - walkingSteadiness - vorsorge - waterIntake - restingHeartRate - walkingSpeed - walkingAsymmetry - walkingStepLength - bmi - bodyTemperature - walkingDoubleSupport - audioExposureEnvironment - audioExposureHeadphone - gripStrength - painNRS - waistCircumference - waistToHeight visible: type: boolean tileVisible: type: boolean order: type: integer minimum: 0 maximum: 99 required: - id - visible - order additionalProperties: false comparisonBaseline: type: string enum: - none - lastMonth - lastYear chartOverlayPrefs: type: object propertyNames: type: string enum: - bp - weight - bmi - pulse - bodyFat - mood - medications - sleep - steps - vo2Max - hrv - restingHr - oxygenSaturation - bodyTemperature - activeEnergy - bloodGlucose - totalBodyWater - boneMass - flightsClimbed - walkingRunningDistance - fatFreeMass - fatMass - muscleMass - skinTemperature - pulseWaveVelocity - vascularAge - visceralFat - audioExposureEnv - audioExposureHeadphone - timeInDaylight - walkingSteadiness - audioExposureEvent - respiratoryRate - leanBodyMass - walkingHeartRateAverage - walkingAsymmetry - walkingDoubleSupport - walkingStepLength - walkingSpeed - cardioRecovery - wristTemperature - fallCount - sixMinuteWalkDistance - stairAscentSpeed - stairDescentSpeed - breathingDisturbances - ansCharge - dayStrain - workoutStrain - cardioLoad - averageHeartRate - maxHeartRate - energyExpenditureKj - gripStrength - painNrs - waistCircumference - waistToHeight additionalProperties: type: object properties: showTrendIndicator: type: boolean showTrendArrow: type: boolean showTargetRange: type: boolean comparisonBaseline: type: string enum: - none - lastMonth - lastYear rangePoints: anyOf: - type: number const: 0 - type: number const: 7 - type: number const: 30 - type: number const: 90 required: - showTrendIndicator - showTrendArrow - showTargetRange additionalProperties: false selectedScoreRings: maxItems: 3 type: array items: type: string enum: - READINESS - RECOVERY_SCORE - SLEEP_SCORE - MED_COMPLIANCE heroRingOrder: maxItems: 4 type: array items: type: string enum: - HEALTH_SCORE - READINESS - RECOVERY_SCORE - SLEEP_SCORE - MED_COMPLIANCE enabledHeroItemKinds: description: Today-highlight item kinds in catalogue order. Missing means every current kind is enabled; an empty array disables all kinds. maxItems: 8 type: array items: type: string enum: - coach_checkin - dose_window - preventive_care - sync_issue - milestone - ecg_new_recording - tension_window - same_time_baseline updatedAt: description: "Optimistic-concurrency token: the stored row's `updatedAt` at read/write time. Echo it back as `baseUpdatedAt` on the next write. Opaque." type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[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: - version additionalProperties: false description: Resolved dashboard layout plus the optimistic-concurrency `updatedAt` token. DashboardLayoutSaved: type: object properties: data: $ref: "#/components/schemas/DashboardLayoutResult" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false DashboardLayoutReset: type: object properties: data: $ref: "#/components/schemas/DashboardLayoutResult" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false AccountPayloadEnvelope: type: object properties: data: $ref: "#/components/schemas/AccountPayload" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false AccountPayload: type: object properties: id: type: string username: type: string accountAccess: $ref: "#/components/schemas/AccountAccess" required: - id - username - accountAccess additionalProperties: {} description: "The signed-in account: its identity, its preferences, and (since v1.36.0) what account sharing lets it do. Additional properties are the preference fields this spec does not yet enumerate. Under an active switch this payload still describes the CALLER — their preferences, their modules, their identity — because display preferences belong to the person at the keyboard rather than to the record they are reading." AccountAccess: type: object properties: accounts: type: array items: $ref: "#/components/schemas/AccountAccessEntry" description: Every record this caller may open, newest grant first. active: anyOf: - $ref: "#/components/schemas/AccountAccessEntry" - type: "null" description: "The record this session is inside right now, resolved to a full entry rather than an id to look up — so a banner can name the person without joining two fields. Null when the caller is in their own record, and ALWAYS null on the Bearer transport: a token carries its selector per request and this endpoint refuses one." canSwitch: type: boolean description: Whether there is anywhere to switch to. Bind it directly. required: - accounts - active - canSwitch additionalProperties: false description: "Account sharing, resolved. This block is the ONLY source of switchability and writability: the server publishes what the caller may do and the client renders it. A client that computed either from grant data would be a second program deciding one person's access to a health record, and the two answer differently the first time an expiry or a revocation appears. Always present — an account nobody has shared with gets an empty list, not a missing field." AccountAccessEntry: type: object properties: accountId: type: string description: The account whose record it is — the value POST /api/account/switch takes, and the value the X-HealthLog-Account header carries. username: type: string displayName: anyOf: - type: string - type: "null" access: type: string enum: - read - write canWrite: type: boolean description: "Whether this caller may ADD to that record: true for an accepted WRITE grant, false for a READ one. It never means edit or delete, which stay with the owner at both levels. Resolved server-side. Render it; never derive it." required: - accountId - username - displayName - access - canWrite additionalProperties: false description: "One account this caller may act on. No avatar is published: the avatar bytes are owner-scoped, so a URL here would resolve to a refusal. Clients paint their initials fallback." RecordActivityEnvelope: type: object properties: data: $ref: "#/components/schemas/RecordActivity" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false RecordActivity: type: object properties: entries: type: array items: $ref: "#/components/schemas/RecordActivityEntry" retentionDays: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: How far back this feed can see, in days. Resolved from the instance's audit retention setting (365 by default, operator-configurable), NOT a constant — an instance running a 90-day window sends 90. Show it beside the rows. required: - entries - retentionDays additionalProperties: false description: The owner's record of what somebody else did in their record, bounded by the instance's audit retention. RecordActivityEntry: type: object properties: id: type: string actor: anyOf: - $ref: "#/components/schemas/AccountGrantParty" - type: "null" description: Who did it. Null when that account has since been deleted — the audit column is deliberately not a foreign key, so history is not rewritten when somebody leaves. Render it as a deleted account; a blank would read as though the owner had done it themselves. action: type: string accesses: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: How many times that delegate opened the record on that day, for a coalesced read row. Null for anything else — a single act has no count, and a 1 there would invite adding up numbers that mean different things. createdAt: type: string required: - id - actor - action - accesses - createdAt additionalProperties: false AccountGrantParty: type: object properties: id: type: string username: type: string displayName: anyOf: - type: string - type: "null" required: - id - username - displayName additionalProperties: false description: "The account on the other end of a grant. E-mail addresses are deliberately not published: a grant is not a way to collect the other party's contact details." AccountGrantListEnvelope: type: object properties: data: $ref: "#/components/schemas/AccountGrantList" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false AccountGrantList: type: object properties: given: type: array items: $ref: "#/components/schemas/AccountGrant" received: type: array items: $ref: "#/components/schemas/AccountGrant" required: - given - received additionalProperties: false description: "Both directions at once: `given` are the grants this account has offered on its own record, `received` are the ones offered to it. Capped at 100 rows per direction, newest first." AccountGrant: type: object properties: id: type: string account: $ref: "#/components/schemas/AccountGrantParty" access: type: string enum: - READ - WRITE state: $ref: "#/components/schemas/AccountGrantState" invitedAt: type: string acceptedAt: anyOf: - type: string - type: "null" expiresAt: anyOf: - type: string - type: "null" lastUsedAt: anyOf: - type: string - type: "null" revokedAt: anyOf: - type: string - type: "null" revokedBy: anyOf: - type: string enum: - GRANTOR - GRANTEE - type: "null" required: - id - account - access - state - invitedAt - acceptedAt - expiresAt - lastUsedAt - revokedAt - revokedBy additionalProperties: false description: "One grant, from either side. The row is also the consent record — who offered the access, when it was accepted, when and by whom it ended — so ended grants stay in the list with `state: REVOKED` rather than disappearing. `revokedBy` distinguishes the owner withdrawing access from the delegate handing it back. `lastUsedAt` mirrors API-token semantics: the last time the grant was actually exercised." AccountGrantState: type: string enum: - PENDING - ACTIVE - EXPIRED - REVOKED description: "What the grant is right now, resolved server-side against the request clock. Only ACTIVE confers anything. Ordering is stated rather than implied: a revoked grant reads REVOKED even if it also sat past its expiry, and an unaccepted invitation past its expiry reads EXPIRED rather than PENDING because it can no longer be accepted. Clients render this value and never re-derive it from the timestamps." AccountGrantEnvelope: type: object properties: data: $ref: "#/components/schemas/AccountGrant" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false AcceptedAccountGrantEnvelope: type: object properties: data: $ref: "#/components/schemas/AccountGrant" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false RevokedAccountGrantEnvelope: type: object properties: data: $ref: "#/components/schemas/EndedAccountGrant" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false EndedAccountGrant: type: object properties: id: type: string account: $ref: "#/components/schemas/AccountGrantParty" access: type: string enum: - READ - WRITE state: $ref: "#/components/schemas/AccountGrantState" invitedAt: type: string acceptedAt: anyOf: - type: string - type: "null" expiresAt: anyOf: - type: string - type: "null" lastUsedAt: anyOf: - type: string - type: "null" revokedAt: anyOf: - type: string - type: "null" revokedBy: anyOf: - type: string enum: - GRANTOR - GRANTEE - type: "null" sessionsCleared: type: integer minimum: -9007199254740991 maximum: 9007199254740991 description: How many of the delegate's browser sessions were sitting inside the record and were put back into their own account, in the same transaction that ended the grant. required: - id - account - access - state - invitedAt - acceptedAt - expiresAt - lastUsedAt - revokedAt - revokedBy - sessionsCleared additionalProperties: false description: "A grant that has just been ended, plus what the cleanup did. Nothing is deleted: the row survives as the consent record and DELETE names the act, not the storage." RenouncedAccountGrantEnvelope: type: object properties: data: $ref: "#/components/schemas/EndedAccountGrant" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false AccountSwitchEnvelope: type: object properties: data: $ref: "#/components/schemas/AccountSwitchState" error: type: "null" meta: type: object properties: requestId: type: string additionalProperties: false required: - data - error additionalProperties: false AccountSwitchState: type: object properties: actingAs: anyOf: - type: object properties: accountId: type: string username: type: string displayName: anyOf: - type: string - type: "null" access: type: string enum: - READ - WRITE required: - accountId - username - displayName - access additionalProperties: false - type: "null" required: - actingAs additionalProperties: false description: The account this session is now acting on, or null when it is back in its own. The stamp is a selector and not a permission — the grant is re-checked on every subsequent request, so a revocation lands on the next one rather than at the next login. MedicationScheduleOutput: type: object properties: id: type: string medicationId: type: string windowStart: type: string description: Legacy single-time-of-intake (HH:mm, user local). Preserved for backwards compatibility; the new `timesOfDay` array supersedes it. windowEnd: type: string description: Legacy reminder-window upper bound (HH:mm). Used to derive the late-classification grace span when `reminderGraceMinutes` is null. label: anyOf: - type: string - type: "null" dose: anyOf: - type: string - type: "null" description: Per-schedule dose override. NULL means the schedule inherits `Medication.dose`. daysOfWeek: anyOf: - type: string - type: "null" description: Legacy persisted recurrence encoding (`null` | `1,3,5` | `i2;1,3,5`). v1.5 readers consult `rrule` first; the field is kept for pre-v1.5 rows. v1.6.0 drops the column. timesOfDay: type: array items: type: string description: v1.5 first-class points-in-time the dose is taken (HH:mm, user local). Backfilled to `[windowStart]` for every pre-v1.5 row. reminderGraceMinutes: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: Reminder grace window in minutes. NULL falls back to the legacy `windowEnd - windowStart` span. rrule: anyOf: - type: string - type: "null" description: RFC 5545 RRULE string (subset). Used for calendar-anchored cadences. **Mutually exclusive with `rollingIntervalDays`** — exactly one of the two is non-null on any v1.5+ schedule (or both are null on legacy rows that haven't been touched since the migration). rollingIntervalDays: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: Flexible-rolling interval in days, counted forward from the latest `MedicationIntakeEvent.takenAt`. **Mutually exclusive with `rrule`.** scheduleType: type: string enum: - SCHEDULED - PRN - CYCLIC description: v1.7.0 schedule-type discriminator. SCHEDULED = rrule / rolling / legacy cadence. PRN = as-needed (never projected, reminded, or counted in compliance expected; still loggable via the intake route). CYCLIC = N weeks on / M weeks off, gating whichever inner cadence the rrule / legacy fields describe. cyclicOnWeeks: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: v1.7.0 cyclic "on" weeks. Only meaningful when `scheduleType` is CYCLIC; null otherwise. cyclicOffWeeks: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: v1.7.0 cyclic "off" weeks. Only meaningful when `scheduleType` is CYCLIC; null otherwise. required: - id - medicationId - windowStart - windowEnd - label - dose - daysOfWeek - timesOfDay - reminderGraceMinutes - rrule - rollingIntervalDays - scheduleType - cyclicOnWeeks - cyclicOffWeeks additionalProperties: false description: Schedule entry attached to a medication. v1.5 promotes `timesOfDay` to first-class and introduces `rrule` (calendar-anchored cadences) and `rollingIntervalDays` (flexible-rolling cadences). The two recurrence primitives are mutually exclusive — enforced by the Zod refine on writes, the route layer, and a DB CHECK constraint (`medication_schedules_rrule_xor_rolling`). v1.7.0 adds `scheduleType` (SCHEDULED / PRN / CYCLIC) and the cyclic on/off-week fields. DisableCoachFlagOutput: type: object properties: disableCoach: type: boolean required: - disableCoach additionalProperties: false description: Per-account Coach opt-out toggle (v1.4.47 W3). `true` hides the Coach FAB and short-circuits its API gates. DiabetesFlagOutput: type: object properties: hasDiabetes: type: boolean required: - hasDiabetes additionalProperties: false description: Per-account diabetes opt-in (v1.18.6). `true` selects the ADA glycemic GOAL bands (fasting 80–130, postprandial < 180) for glucose targets instead of the general non-diabetic bands. A user-declared preference only — never inferred from a value, never a diagnosis. TourProgressOutput: type: object properties: lastStopId: anyOf: - type: string minLength: 1 maxLength: 64 - type: "null" completedStopIds: default: [] maxItems: 64 type: array items: type: string minLength: 1 maxLength: 64 status: type: string enum: - in_progress - completed - skipped 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))$ required: - lastStopId - completedStopIds - status - updatedAt additionalProperties: false description: Resumable module-tour progress point. `lastStopId` seeds the resume index; `status` is the running/terminal state. 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. reorderLeadDays: anyOf: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 - type: "null" description: v1.17.0 — optional per-medication reorder lead time in days (0–60). The low-stock alert widens its trigger by this lead plus one dose-interval so a refill arrives before the last dose. null = inherit the user-level notificationPrefs.medication.reorderLeadDays default (10). 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: description: "Present on the READ paths only — the list and single GET compute it; the create / update responses return the stored row and omit it. 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." 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" nextDueOverdue: description: "Present on the READ paths only, alongside `nextDueAt`. 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." type: boolean 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 - reorderLeadDays - active - notificationsEnabled - liveActivityEnabled - criticalAlarmEnabled - atcCode - rxNormCode - pausedAt - snoozedUntil - 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. MedicationDoseHistoryImportFatalReason: type: string enum: - empty_file - missing_required_columns - unreadable_json - json_not_an_array - json_carries_no_intake_time description: Whole-file refusal reason on the 422 `errorCode`. empty_file — no data rows. missing_required_columns — the CSV header is missing a column the parser needs. unreadable_json — the body did not parse as JSON. json_not_an_array — parsed JSON is not an array (and no nested array was found under any key). json_carries_no_intake_time — well-formed JSON export input, refused by design (see above). CoachPrefs: 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 reminderSuggestions: type: object properties: enabled: default: true type: boolean stopped: default: false type: boolean dismissedCadences: default: [] maxItems: 32 type: array items: type: string maxLength: 64 lastSuggestedAt: default: null anyOf: - type: string maxLength: 40 - type: "null" required: - enabled - stopped - dismissedCadences - lastSuggestedAt additionalProperties: false required: - tone - verbosity - excludeMetrics - showEvidenceByDefault - defaultWindow additionalProperties: false parameters: AccountSelector: name: X-HealthLog-Account in: header required: false schema: type: string maxLength: 64 description: "Act on another account's record for THIS request. Bearer transport only — the browser session carries its switch on the session row, and sending this header alongside a cookie is refused rather than ignored. The value is an `accountId` from `accountAccess.accounts` on GET /api/auth/me. It is a SELECTOR and not a permission: the grant is re-checked on every request, so a revocation lands on the next one rather than at the next login, and a selector naming an account that granted nothing is refused with 403 `sharing.access.denied` — byte-identical to one naming an account that does not exist, so the refusal is not an enumeration oracle. NEVER attach it to the sync engine, device or notification registration, or any auth/refresh call: those are about the person holding the token, not the record being read, and the account payload refuses the header outright for exactly that reason." 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.