Files
HealthLog/docs/api/openapi.yaml
T
Marc-André BombeckandClaude Opus 4.8 6745850f16 chore(release): v1.15.0 — cycle tracking
Cut v1.15.0: the private menstrual-cycle vertical. Quick-entry day-log
(period/flow, BBT, mucus, OPK + pregnancy/progesterone tests,
contraception, intercourse, graded symptoms), a calendar + signature
cycle wheel, honest range predictions with confidence + a read-your-body
mode, a BBT chart, phase×vitals + symptom-by-phase insights, opt-in
per-channel reminders, encryption of intent fields, discreet
notifications, a one-tap data purge, and Apple-Health reproductive import
+ health-record export. Predictions run on a deterministic engine the iOS
client mirrors offline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 13:52:21 +02:00

11602 lines
393 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
openapi: 3.1.0
info:
title: HealthLog API
version: 1.15.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: AGPL-3.0
url: https://github.com/MBombeck/HealthLog/blob/main/LICENSE
contact:
name: HealthLog
url: https://healthlog.dev
servers:
- url: https://healthlog.bombeck.io
description: Production
- url: http://localhost:3000
description: Local dev
tags:
- name: Auth
- name: Measurements
- name: Medications
- name: Mood
- name: Analytics
- name: Insights
- name: Dashboard
- name: Notifications
- name: Devices
- name: Export
- name: Sync
- name: Cycle
- name: Admin
- name: Meta
paths:
/api/cycle/day-logs:
get:
tags:
- Cycle
summary: Read a single day's cycle day-log (v1.15.0)
description: Returns the full `CycleDayLogDTO` for the tz-anchored `date`, or `null` when nothing is logged that day.
Lets a client pre-fill an edit sheet. Gated; owner-scoped; soft-deleted rows excluded.
parameters:
- in: query
name: date
schema:
type: string
pattern: ^\d{4}-\d{2}-\d{2}$
required: true
responses:
"200":
description: The day-log for that date, or null.
content:
application/json:
schema:
$ref: "#/components/schemas/CycleDayLogReadEnvelope"
"401": &a1
description: Authentication required or invalid credentials.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"403": &a2
description: Cycle tracking is not enabled for this account (errorCode `cycle.disabled`).
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": &a3
description: Request validation failed.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"429": &a4
description: Rate limit exceeded.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
post:
tags:
- Cycle
summary: Capture a single cycle day-log (v1.15.0)
description: "Upserts on `(userId, source, externalId)` when externalId present, else `(userId, date)`. `note` encrypts
at rest. 201 on insert, 200 on update. Gated: `cycle.disabled` 403 when the feature is off."
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
date:
type: string
pattern: ^\d{4}-\d{2}-\d{2}$
flow:
type: string
enum:
- NONE
- SPOTTING
- LIGHT
- MEDIUM
- HEAVY
intermenstrualBleeding:
type: boolean
basalBodyTempC:
type: number
minimum: 30
maximum: 45
ovulationTest:
type: string
enum:
- NEGATIVE
- POSITIVE_LH_SURGE
- ESTROGEN_SURGE
- INDETERMINATE
cervicalMucus:
type: string
enum:
- DRY
- STICKY
- CREAMY
- WATERY
- EGG_WHITE
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"
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"
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:<userId>` 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
ovulationTest:
type: string
enum:
- NEGATIVE
- POSITIVE_LH_SURGE
- ESTROGEN_SURGE
- INDETERMINATE
cervicalMucus:
type: string
enum:
- DRY
- STICKY
- CREAMY
- WATERY
- EGG_WHITE
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
rawChartMode:
type: boolean
predictionEnabled:
type: boolean
discreetNotifications:
type: boolean
sensitiveCategoryEncryption:
type: boolean
typicalCycleLength:
anyOf:
- type: integer
minimum: 15
maximum: 60
- type: "null"
typicalPeriodLength:
anyOf:
- type: integer
minimum: 1
maximum: 15
- type: "null"
lutealPhaseLength:
anyOf:
- type: integer
minimum: 10
maximum: 16
- type: "null"
responses:
"200":
description: Merged cycle preferences.
content:
application/json:
schema:
$ref: "#/components/schemas/CyclePrefsPatchEnvelope"
"401": *a1
"422": *a3
"429": *a4
/api/meta/capabilities:
get:
tags:
- Meta
summary: Live server capability / id-vocabulary discovery
description: Returns the server's REAL id vocabularies (derived-metric ids, vitals-baseline types, layout tile-ids,
metric-status ids, the HealthKit ingest mapping, the FHIR coding constants) plus the running API contract
version. Every list is derived server-side from the canonical registry it documents, so a client can gate its UI
/ decoder against what the server actually ships rather than a hand-maintained copy. Auth via cookie or Bearer
(not admin).
responses:
"200":
description: Capability snapshot.
content:
application/json:
schema:
$ref: "#/components/schemas/CapabilitiesEnvelope"
"401": *a1
"422": *a3
"429": *a4
/api/export/health-record:
post:
tags:
- Export
summary: Generate a health-record export (PDF / FHIR / package)
description: "v1.7.0 flagship export. Returns the doctor-handover artefact in the requested `format`: `pdf` →
application/pdf, `fhir` → application/fhir+json (HL7 FHIR R4 document Bundle), `package` → application/zip (PDF
+ FHIR + README). Auth via cookie or Bearer; shared `export:<userId>` rate bucket (10/h). Strict validation:
unknown keys 422."
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
format:
type: string
enum:
- pdf
- fhir
- package
range:
type: object
properties:
startDate:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
endDate:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
days:
type: integer
minimum: 1
maximum: 365
additionalProperties: false
sections:
type: object
properties:
vitals:
type: object
properties:
weight:
type: boolean
bp:
type: boolean
pulse:
type: boolean
oxygenSaturation:
type: boolean
bodyFat:
type: boolean
bodyComposition:
type: boolean
cardioFitness:
type: object
properties:
restingHeartRate:
type: boolean
hrv:
type: boolean
vo2max:
type: boolean
activity:
type: object
properties:
steps:
type: boolean
distance:
type: boolean
energy:
type: boolean
sleep:
type: boolean
glucose:
type: boolean
medications:
type: object
properties:
list:
type: boolean
compliance:
type: boolean
glp1:
type: boolean
sideEffects:
type: boolean
mood:
type: boolean
bmi:
type: boolean
cycle:
type: boolean
locale:
type: string
enum:
- de
- en
- fr
- es
- it
- pl
practiceName:
type: string
maxLength: 120
includeCharts:
type: boolean
includeAiSummary:
type: boolean
germanAtc:
type: boolean
required:
- format
additionalProperties: false
responses:
"200":
description: "Export generated. Content-Type varies by `format`: application/pdf, application/fhir+json, or
application/zip."
content:
application/pdf:
schema:
type: string
format: binary
application/fhir+json:
schema:
type: string
format: binary
application/zip:
schema:
type: string
format: binary
"401": *a1
"422": *a3
"429": *a4
/api/share-links:
post:
tags:
- Export
summary: Create a clinician share link (v1.11.0)
description: "Owner-only. Mints an `hls_` token (192-bit), stores only its HMAC hash, and returns the raw token EXACTLY
ONCE in the response. Every scope column (window, sections, FHIR resource types, API toggle) is frozen
write-once. `expiresAt` is required and capped at 90 days. Auth via cookie or Bearer; rate-limited
(`share-link:<userId>`, 20/h). Strict: unknown keys 422."
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
label:
type: string
minLength: 1
maxLength: 120
rangeStart:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
rangeEnd:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
sections:
type: object
properties:
vitals:
type: object
properties:
weight:
type: boolean
bp:
type: boolean
pulse:
type: boolean
oxygenSaturation:
type: boolean
bodyFat:
type: boolean
bodyComposition:
type: boolean
cardioFitness:
type: object
properties:
restingHeartRate:
type: boolean
hrv:
type: boolean
vo2max:
type: boolean
activity:
type: object
properties:
steps:
type: boolean
distance:
type: boolean
energy:
type: boolean
sleep:
type: boolean
glucose:
type: boolean
medications:
type: object
properties:
list:
type: boolean
compliance:
type: boolean
glp1:
type: boolean
sideEffects:
type: boolean
mood:
type: boolean
bmi:
type: boolean
cycle:
type: boolean
resourceTypes:
maxItems: 8
type: array
items:
type: string
enum:
- Patient
- Observation
- MedicationStatement
- MedicationAdministration
allowFhirApi:
type: boolean
expiresAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
required:
- label
- rangeStart
- expiresAt
additionalProperties: false
responses:
"201":
description: Share link created. `token` carries the raw `hls_` value and is unrecoverable after this response.
content:
application/json:
schema:
$ref: "#/components/schemas/ShareLinkCreated"
"401": *a1
"422": *a3
"429": *a4
get:
tags:
- Export
summary: List own clinician share links (v1.11.0)
description: Owner-only. Returns the caller's own share links (never the raw token — it is unrecoverable after
creation). Auth via cookie or Bearer.
responses:
"200":
description: Share links owned by the caller.
content:
application/json:
schema:
$ref: "#/components/schemas/ShareLinkList"
"401": *a1
"422": *a3
"429": *a4
/api/share-links/{id}:
delete:
tags:
- Export
summary: Revoke a clinician share link (v1.11.0)
description: Owner-only. Sets `revokedAt` on the caller's own link. A cross-user or unknown id is sealed as 404. Auth
via cookie or Bearer; rate-limited.
parameters:
- in: path
name: id
schema:
type: string
required: true
responses:
"200":
description: Link revoked.
content:
application/json:
schema:
$ref: "#/components/schemas/ShareLinkRevoked"
"401": *a1
"404":
description: Link not found (or owned by another user).
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a3
"429": *a4
/api/fhir/metadata:
get:
tags:
- FHIR
summary: FHIR R4 CapabilityStatement (v1.11.0)
description: "Read-only FHIR R4 capability statement for the REST face. Declares the served resource types (Patient,
Observation, MedicationStatement, MedicationAdministration), the `$everything` operation, and the
`application/fhir+json` format. Auth: `fhir:read` scope (cookie sessions also pass)."
responses:
"200":
description: CapabilityStatement (application/fhir+json).
content:
application/fhir+json:
schema:
type: string
format: binary
"401": *a1
"422": *a3
"429": *a4
/api/fhir/Patient:
get:
tags:
- FHIR
summary: FHIR R4 Patient search (v1.11.0)
description: "Read-only `searchset` Bundle of the caller's own Patient resource. Auth: `fhir:read` scope. Offset paging
via `_count` (clamped ≤200) / `_offset`. `userId` is narrowed from auth."
parameters:
- in: query
name: _count
schema:
type: number
- in: query
name: _offset
schema:
type: number
responses:
"200":
description: searchset Bundle (application/fhir+json).
content:
application/fhir+json:
schema:
type: string
format: binary
"401": *a1
"422": *a3
"429": *a4
/api/fhir/Observation:
get:
tags:
- FHIR
summary: FHIR R4 Observation search (v1.11.0)
description: "Read-only `searchset` Bundle of the caller's own Observations (vitals / activity / lab / survey). Auth:
`fhir:read` scope. Offset paging via `_count` (clamped ≤200) / `_offset`."
parameters:
- in: query
name: _count
schema:
type: number
- in: query
name: _offset
schema:
type: number
responses:
"200":
description: searchset Bundle (application/fhir+json).
content:
application/fhir+json:
schema:
type: string
format: binary
"401": *a1
"422": *a3
"429": *a4
/api/fhir/MedicationStatement:
get:
tags:
- FHIR
summary: FHIR R4 MedicationStatement search (v1.11.0)
description: "Read-only `searchset` Bundle of the caller's own active-medication statements. Auth: `fhir:read` scope.
Offset paging via `_count` (≤200) / `_offset`."
parameters:
- in: query
name: _count
schema:
type: number
- in: query
name: _offset
schema:
type: number
responses:
"200":
description: searchset Bundle (application/fhir+json).
content:
application/fhir+json:
schema:
type: string
format: binary
"401": *a1
"422": *a3
"429": *a4
/api/fhir/MedicationAdministration:
get:
tags:
- FHIR
summary: FHIR R4 MedicationAdministration search (v1.11.0)
description: "Read-only `searchset` Bundle of the caller's own acted intakes (completed / not-done). Auth: `fhir:read`
scope. Offset paging via `_count` (≤200) / `_offset`."
parameters:
- in: query
name: _count
schema:
type: number
- in: query
name: _offset
schema:
type: number
responses:
"200":
description: searchset Bundle (application/fhir+json).
content:
application/fhir+json:
schema:
type: string
format: binary
"401": *a1
"422": *a3
"429": *a4
/api/fhir/$everything:
get:
tags:
- FHIR
summary: FHIR R4 $everything (v1.11.0)
description: "Read-only `$everything` operation: every resource in the caller's own record (Patient, Coverage,
Observations, MedicationStatements, MedicationAdministrations) in one `searchset` Bundle. Auth: `fhir:read`
scope. Offset paging via `_count` (≤200) / `_offset`."
parameters:
- in: query
name: _count
schema:
type: number
- in: query
name: _offset
schema:
type: number
responses:
"200":
description: searchset Bundle (application/fhir+json).
content:
application/fhir+json:
schema:
type: string
format: binary
"401": *a1
"422": *a3
"429": *a4
/api/auth/login:
post:
tags:
- Auth
summary: Email-or-username login (password)
description: "Browser callers receive a session cookie. Native callers (X-Client-Type: native or HealthLog-iOS UA
prefix) additionally receive a paired access + refresh token."
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
email:
type: string
minLength: 1
password:
type: string
minLength: 1
required:
- email
- password
responses:
"200":
description: Login succeeded.
content:
application/json:
schema:
$ref: "#/components/schemas/LoginResponse"
"401": *a1
"422": *a3
"429": *a4
/api/auth/passkey/login-verify:
post:
tags:
- Auth
summary: Passkey assertion verification
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/PasskeyLoginVerifyRequest"
responses:
"200":
description: Assertion verified — session + optional bearer issued.
content:
application/json:
schema:
$ref: "#/components/schemas/PasskeyLoginVerifyResponse"
"401": *a1
"422": *a3
"429": *a4
/api/auth/refresh:
post:
tags:
- Auth
summary: Rotate refresh token (one-time use)
description: >-
Reuse of a consumed refresh token revokes every refresh token still active for the originating device
(per-device blast radius from v1.4.23). Legacy tokens issued before v1.4.23 with a null deviceId fall back to
revoke-all-for-user.
On a 401, `meta.errorCode` is a stable machine code so the client can branch terminal re-auth from a transient
blip without parsing the prose `error`: `auth.refresh.reuse` (a consumed token was replayed — device family
revoked, re-pair required), `auth.refresh.revoked` (family revoked out-of-band — re-pair required),
`auth.refresh.invalid` (not found / expired — drop the token and re-authenticate).
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RefreshTokenRequest"
responses:
"200":
description: Rotation succeeded — new pair issued.
content:
application/json:
schema:
$ref: "#/components/schemas/RefreshResponse"
"401": *a1
"422": *a3
"429": *a4
/api/sync/state:
get:
tags:
- Sync
summary: Sync handshake + window metadata
description: Cheap 'should I sync?' summary. Returns the previous `lastSyncedAt` checkpoint and advances it server-side
on each call. The `sync` block carries the incremental-delta window + tombstone retention so the client reads
them rather than hardcoding.
responses:
"200":
description: Sync state summary.
content:
application/json:
schema:
$ref: "#/components/schemas/SyncStateEnvelope"
"401": *a1
"422": *a3
"429": *a4
/api/sync/changes:
get:
tags:
- Sync
summary: Measurements delta feed
description: "Incremental catch-up after the first-pair backfill (never a replacement for it). Pages over an opaque
keyset cursor; each page carries `tombstones` (soft-deleted rows, keyed on `externalId`) and `upserts` (live
rows). Apply tombstones before upserts within a page. `cursorExpired: true` forces a clean re-init."
parameters:
- in: query
name: cursor
schema:
description: Opaque multi-domain keyset cursor from the previous page. Treat as fully opaque — echo, never parse. Omit
for the initial sync.
type: string
minLength: 1
maxLength: 2048
description: Opaque multi-domain keyset cursor from the previous page. Treat as fully opaque — echo, never parse. Omit
for the initial sync.
- in: query
name: limit
schema:
description: Page size, default 200, hard cap 500.
type: integer
minimum: 1
maximum: 500
description: Page size, default 200, hard cap 500.
responses:
"200":
description: Delta page.
content:
application/json:
schema:
$ref: "#/components/schemas/SyncChangesEnvelope"
"401": *a1
"422": *a3
"429": *a4
/api/measurements:
get:
tags:
- Measurements
summary: List measurements
description: Filter by type + date range. Response is paged via offset/limit; default limit 100, hard cap 500.
parameters:
- in: query
name: type
schema:
type: string
enum:
- WEIGHT
- BLOOD_PRESSURE_SYS
- BLOOD_PRESSURE_DIA
- PULSE
- BODY_FAT
- SLEEP_DURATION
- ACTIVITY_STEPS
- BLOOD_GLUCOSE
- TOTAL_BODY_WATER
- BONE_MASS
- OXYGEN_SATURATION
- HEART_RATE_VARIABILITY
- RESTING_HEART_RATE
- ACTIVE_ENERGY_BURNED
- FLIGHTS_CLIMBED
- WALKING_RUNNING_DISTANCE
- VO2_MAX
- BODY_TEMPERATURE
- FAT_FREE_MASS
- FAT_MASS
- MUSCLE_MASS
- SKIN_TEMPERATURE
- PULSE_WAVE_VELOCITY
- VASCULAR_AGE
- VISCERAL_FAT
- AUDIO_EXPOSURE_ENV
- AUDIO_EXPOSURE_HEADPHONE
- TIME_IN_DAYLIGHT
- WALKING_STEADINESS
- AUDIO_EXPOSURE_EVENT
- RESPIRATORY_RATE
- BODY_MASS_INDEX
- LEAN_BODY_MASS
- WALKING_HEART_RATE_AVERAGE
- WALKING_ASYMMETRY
- WALKING_DOUBLE_SUPPORT
- WALKING_STEP_LENGTH
- WALKING_SPEED
- CARDIO_RECOVERY
- WRIST_TEMPERATURE
- FALL_COUNT
- SIX_MINUTE_WALK_DISTANCE
- STAIR_ASCENT_SPEED
- STAIR_DESCENT_SPEED
- BREATHING_DISTURBANCES
- IRREGULAR_RHYTHM_NOTIFICATION
- HIGH_HEART_RATE_EVENT
- LOW_HEART_RATE_EVENT
- WALKING_STEADINESS_EVENT
- BREATHING_DISTURBANCE_EVENT
- RECOVERY_SCORE
- STRESS_SCORE
- STRAIN_SCORE
- HRV_RMSSD
- DAY_STRAIN
- WORKOUT_STRAIN
- SLEEP_PERFORMANCE
- SLEEP_EFFICIENCY
- SLEEP_CONSISTENCY
- SLEEP_NEED
- ENERGY_EXPENDITURE_KJ
- AVERAGE_HEART_RATE
- MAX_HEART_RATE
- SLEEP_DISTURBANCE_COUNT
- in: query
name: from
schema:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- in: query
name: to
schema:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- in: query
name: limit
schema:
default: 100
type: integer
minimum: 1
maximum: 5000
- in: query
name: offset
schema:
default: 0
type: integer
minimum: 0
maximum: 9007199254740991
- in: query
name: sortBy
schema:
default: measuredAt
type: string
enum:
- type
- value
- measuredAt
- source
- in: query
name: sortDir
schema:
default: desc
type: string
enum:
- asc
- desc
- in: query
name: aggregate
schema:
type: string
enum:
- raw
- daily
- weekly
- monthly
- in: query
name: source
schema:
type: string
enum:
- rollup
- in: query
name: groupBy
schema:
type: string
enum:
- day
- in: query
name: dayKey
schema:
type: string
pattern: ^\d{4}-\d{2}-\d{2}$
responses:
"200":
description: Measurement page.
content:
application/json:
schema:
$ref: "#/components/schemas/ListMeasurementsResponse"
"401": *a1
"422": *a3
"429": *a4
post:
tags:
- Measurements
summary: Create one measurement
description: Single ingest. Use `/api/measurements/batch` for Apple Health upload streams.
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
type:
type: string
enum:
- WEIGHT
- BLOOD_PRESSURE_SYS
- BLOOD_PRESSURE_DIA
- PULSE
- BODY_FAT
- SLEEP_DURATION
- ACTIVITY_STEPS
- BLOOD_GLUCOSE
- TOTAL_BODY_WATER
- BONE_MASS
- OXYGEN_SATURATION
- HEART_RATE_VARIABILITY
- RESTING_HEART_RATE
- ACTIVE_ENERGY_BURNED
- FLIGHTS_CLIMBED
- WALKING_RUNNING_DISTANCE
- VO2_MAX
- BODY_TEMPERATURE
- FAT_FREE_MASS
- FAT_MASS
- MUSCLE_MASS
- SKIN_TEMPERATURE
- PULSE_WAVE_VELOCITY
- VASCULAR_AGE
- VISCERAL_FAT
- AUDIO_EXPOSURE_ENV
- AUDIO_EXPOSURE_HEADPHONE
- TIME_IN_DAYLIGHT
- WALKING_STEADINESS
- AUDIO_EXPOSURE_EVENT
- RESPIRATORY_RATE
- BODY_MASS_INDEX
- LEAN_BODY_MASS
- WALKING_HEART_RATE_AVERAGE
- WALKING_ASYMMETRY
- WALKING_DOUBLE_SUPPORT
- WALKING_STEP_LENGTH
- WALKING_SPEED
- CARDIO_RECOVERY
- WRIST_TEMPERATURE
- FALL_COUNT
- SIX_MINUTE_WALK_DISTANCE
- STAIR_ASCENT_SPEED
- STAIR_DESCENT_SPEED
- BREATHING_DISTURBANCES
- IRREGULAR_RHYTHM_NOTIFICATION
- HIGH_HEART_RATE_EVENT
- LOW_HEART_RATE_EVENT
- WALKING_STEADINESS_EVENT
- BREATHING_DISTURBANCE_EVENT
- RECOVERY_SCORE
- STRESS_SCORE
- STRAIN_SCORE
- HRV_RMSSD
- DAY_STRAIN
- WORKOUT_STRAIN
- SLEEP_PERFORMANCE
- SLEEP_EFFICIENCY
- SLEEP_CONSISTENCY
- SLEEP_NEED
- ENERGY_EXPENDITURE_KJ
- AVERAGE_HEART_RATE
- MAX_HEART_RATE
- SLEEP_DISTURBANCE_COUNT
value:
type: number
measuredAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
notes:
type: string
maxLength: 200
source:
default: MANUAL
type: string
enum:
- MANUAL
- APPLE_HEALTH
glucoseContext:
type: string
enum:
- FASTING
- POSTPRANDIAL
- RANDOM
- BEDTIME
deviceType:
anyOf:
- type: string
minLength: 1
maxLength: 32
- type: "null"
required:
- type
- value
- measuredAt
responses:
"201":
description: Created.
content:
application/json:
schema:
$ref: "#/components/schemas/CreateMeasurementResponse"
"401": *a1
"422": *a3
"429": *a4
/api/measurements/batch:
post:
tags:
- Measurements
summary: Apple Health batch ingest
description: Up to 500 HealthKit entries per call. Idempotent via the `Idempotency-Key` header (replay window 24h).
Per-entry status lets the iOS client advance its sync cursor accurately. v1.4.25 W8c adds an optional
`deviceType` per entry — feed it from `HKDevice.model` so the cross-source canonical picker can break
Apple-Watch-vs-iPhone ties; null/absent stays backward-compatible with v1.4.23 clients.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/AppleHealthBatchRequest"
responses:
"200":
description: Batch processed.
content:
application/json:
schema:
$ref: "#/components/schemas/BatchMeasurementsResponse"
"401": *a1
"422": *a3
"429": *a4
/api/measurements/by-external-ids:
delete:
tags:
- Measurements
summary: Delete measurements by external ID (iOS deletion-sync)
description: Removes the user's measurement rows whose externalId is in the request list. Rows owned by another user are
silently skipped (cross-user 404 guard). Up to 500 externalIds per call.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MeasurementsDeleteByExternalIdsRequest"
responses:
"200":
description: Delete batch processed.
content:
application/json:
schema:
$ref: "#/components/schemas/MeasurementsDeleteByExternalIdsResponse"
"401": *a1
"422": *a3
"429": *a4
/api/sleep/night:
get:
tags:
- Measurements
summary: Reconstructed sleep night (hypnogram source, v1.11.5)
description: "Returns one night's reconstructed sleep session for the phase-progression (hypnogram) view: the canonical
source's stage segments (each with an absolute start/end), the per-stage breakdown, asleep/in-bed/awake totals,
the mid-sleep awakenings count, and same-wake-day naps surfaced separately. Session-clustered, keyed by the
local wake day, collapsed to one source via the sleep priority ladder so two sources never overlay. `date`
omitted returns the most recent night. A read-only view over existing per-stage SLEEP_DURATION rows — no schema,
no new measurement type."
parameters:
- in: query
name: date
schema:
description: Wake-day key (YYYY-MM-DD). Omit for the most recent night.
type: string
pattern: ^\d{4}-\d{2}-\d{2}$
description: Wake-day key (YYYY-MM-DD). Omit for the most recent night.
responses:
"200":
description: Reconstructed night.
content:
application/json:
schema:
$ref: "#/components/schemas/SleepNightResponse"
"401": *a1
"422": *a3
"429": *a4
/api/workouts:
get:
tags:
- Measurements
summary: List workouts (v1.4.32)
description: "Paginated workout list with cross-source canonical-row dedup. The picker reads the per-user
source-priority ladder and buckets by `(startedAt 5 min slot, sportType)` — twin workouts (Apple Watch +
Withings ScanWatch) collapse to a single canonical row per cluster. Field names mirror the iOS handoff contract:
`distanceM`, `activeEnergyKcal`, `avgHr`, `maxHr`."
parameters:
- in: query
name: limit
schema:
type: integer
minimum: 1
maximum: 200
- in: query
name: offset
schema:
type: integer
minimum: 0
maximum: 9007199254740991
- in: query
name: since
schema:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- in: query
name: until
schema:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- in: query
name: sportType
schema:
type: string
responses:
"200":
description: Canonical workout page.
content:
application/json:
schema:
$ref: "#/components/schemas/ListWorkoutsResponse"
"401": *a1
"422": *a3
"429": *a4
/api/workouts/{id}:
get:
tags:
- Measurements
summary: Workout detail (v1.4.32)
description: Single-workout envelope. Owns the optional `WorkoutRoute` GeoJSON geometry + `canonicalId` pointer that
resolves to the cluster winner so deep-links into non-canonical twin rows can redirect cleanly. Cross-user rows
surface as 404 (existence channel sealed).
parameters:
- in: path
name: id
schema:
type: string
required: true
responses:
"200":
description: Workout detail.
content:
application/json:
schema:
$ref: "#/components/schemas/GetWorkoutResponse"
"401": *a1
"404":
description: Workout not found (or owned by another user).
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a3
"429": *a4
/api/workouts/batch:
post:
tags:
- Measurements
summary: Typed workout batch ingest (v1.4.25 W16b)
description: Server-side ingest endpoint for HKWorkout records (iOS) and Withings activity rows (server-to-server). Up
to 100 workouts per call; nested route geometry (GeoJSON LineString) is capped at 20 000 points and stored in a
1:1 `WorkoutRoute` row keyed by `workoutId`. Idempotent via the `Idempotency-Key` header (replay window 24h).
Per-entry status (`inserted | duplicate | skipped`) lets the iOS sync cursor checkpoint accurately. The request
body ceiling is 5 MB enforced at the HTTP layer — clients above the ceiling receive a 413 with
`workout.batch.payload_too_large`.
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
workouts:
minItems: 1
maxItems: 100
type: array
items:
type: object
properties:
sportType:
type: string
enum:
- walking
- running
- cycling
- hiking
- swimming
- rowing
- elliptical
- stairClimber
- yoga
- mindAndBody
- strength
- hiit
- dance
- golf
- tennis
- basketball
- soccer
- crossTraining
- mixedCardio
- other
startedAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
endedAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
totalEnergyKcal:
type: number
minimum: 0
maximum: 50000
totalDistanceM:
type: number
minimum: 0
maximum: 1000000
avgHeartRate:
type: integer
minimum: 20
maximum: 300
maxHeartRate:
type: integer
minimum: 20
maximum: 300
minHeartRate:
type: integer
minimum: 20
maximum: 300
stepCount:
type: integer
minimum: 0
maximum: 200000
elevationM:
type: number
minimum: -500
maximum: 10000
pauseDurationSec:
type: integer
minimum: 0
maximum: 86400
source:
default: MANUAL
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
externalId:
type: string
maxLength: 128
externalSourceVersion:
type: string
maxLength: 64
metadata:
type: object
propertyNames:
type: string
additionalProperties: {}
route:
type: object
properties:
geometry:
type: object
properties:
type:
type: string
const: LineString
coordinates:
minItems: 2
maxItems: 20000
type: array
items:
type: array
prefixItems:
- type: number
minimum: -180
maximum: 180
- type: number
minimum: -90
maximum: 90
items:
type: number
required:
- type
- coordinates
sampleTimestamps:
type: array
items:
type: object
properties:
t:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
speedMs:
type: number
minimum: 0
maximum: 50
hr:
type: integer
minimum: 20
maximum: 300
required:
- t
required:
- geometry
samples:
minItems: 1
maxItems: 30000
type: array
items:
type: object
properties:
t:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
hr:
type: integer
minimum: 20
maximum: 300
speedMs:
type: number
minimum: 0
maximum: 50
power:
type: number
minimum: 0
maximum: 3000
cadence:
type: number
minimum: 0
maximum: 400
required:
- t
required:
- sportType
- startedAt
- endedAt
required:
- workouts
responses:
"200":
description: Batch processed.
content:
application/json:
schema:
$ref: "#/components/schemas/BatchWorkoutsResponse"
"400":
description: Batch validation failed (over-cap, schema reject, or oversized route).
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"401": *a1
"413":
description: Request body exceeds the 5 MB ceiling.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a3
"429": *a4
/api/devices:
post:
tags:
- Devices
summary: Register native device + APNs token
description: Idempotent upsert by `token`. APNs token + environment are paired — supplying one without the other returns
422. Cross-user re-registration of either identifier returns 409.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DeviceRegisterRequest"
responses:
"201":
description: Device registered or refreshed.
content:
application/json:
schema:
$ref: "#/components/schemas/DeviceRegisterResponse"
"401": *a1
"409":
description: Device or APNs token already registered to another user.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a3
"429": *a4
/api/medications:
get:
tags:
- Medications
summary: List medications for the calling user
description: Returns every medication owned by the caller (active + paused), ordered by `createdAt DESC`. Each row
carries its nested `schedules`, the joined clinical `category`, the latest non-skipped `lastTakenAt`, and the
count of intake events scheduled for today (`todayEventCount`). The response is cached server-side for 60 s per
user; writes flush the cache.
responses:
"200":
description: Medication list.
content:
application/json:
schema:
$ref: "#/components/schemas/ListMedicationsResponse"
"401": *a1
"422": *a3
"429": *a4
post:
tags:
- Medications
summary: Create a medication with at least one schedule
description: Validates the body against `CreateMedicationRequest`, applies the v1.5 cross-field invariants (one-shot
consistency, recurring default `FREQ=DAILY`, `timesOfDay` dual-write), and creates the medication + its
schedules in a single Prisma write. Audits as `medication.create`.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateMedicationRequest"
responses:
"201":
description: Created medication with its schedules.
content:
application/json:
schema:
$ref: "#/components/schemas/CreateMedicationResponse"
"401": *a1
"422": *a3
"429": *a4
/api/medications/{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:<id>:ingest`. Audits as `medication.delete`.
parameters:
- in: path
name: id
schema:
type: string
required: true
responses:
"200":
description: Deletion succeeded.
content:
application/json:
schema:
$ref: "#/components/schemas/DeleteMedicationResponse"
"401": *a1
"404":
description: Medication not found (or owned by another user).
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *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/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/{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/auth/me/coach-prefs:
get:
tags:
- Auth
summary: Read per-user Coach prompt-tuning preferences
description: Returns the persisted preferences with defaults filled in. Null persisted state resolves to the legacy
v1.4.22 defaults.
responses:
"200":
description: Resolved preferences.
content:
application/json:
schema:
$ref: "#/components/schemas/GetCoachPrefsResponse"
"401": *a1
"422": *a3
"429": *a4
put:
tags:
- Auth
summary: Replace per-user Coach prompt-tuning preferences
description: Persists the supplied prefs. Body is validated against `CoachPrefs`; missing keys fall back to the
documented defaults.
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
tone:
default: warm
type: string
enum:
- warm
- neutral
- concise
verbosity:
default: default
type: string
enum:
- brief
- default
- detailed
excludeMetrics:
default: []
maxItems: 11
type: array
items:
type: string
enum:
- bp
- weight
- pulse
- mood
- compliance
- hrv
- sleep
- resting_hr
- steps
- medications
- anthropometrics
showEvidenceByDefault:
default: false
type: boolean
defaultWindow:
default: allTime
type: string
enum:
- last7days
- last30days
- last90days
- allTime
dataClusters:
maxItems: 10
type: array
items:
type: string
enum:
- cardio
- body
- activity
- workouts
- sleep
- mood
- glucose
- medication
- mobility
- environment
responses:
"200":
description: Saved preferences echoed back.
content:
application/json:
schema:
$ref: "#/components/schemas/PutCoachPrefsResponse"
"401": *a1
"422": *a3
"429": *a4
/api/auth/me/disable-coach:
get:
tags:
- Auth
summary: Read per-user Coach opt-out flag
description: Returns the user's per-account Coach opt-out flag. Default `false` (Coach visible). Powers the Settings →
Insights "Hide Coach" Switch and the layout FAB short-circuit.
responses:
"200":
description: Resolved flag.
content:
application/json:
schema:
$ref: "#/components/schemas/GetDisableCoachResponse"
"401": *a1
"422": *a3
"429": *a4
patch:
tags:
- Auth
summary: Toggle per-user Coach opt-out flag
description: Flips the per-account Coach opt-out flag. Idempotent — the DB write fires even when the value matches so
the audit-log row mirrors the API call. Rate-limit 60/min per user.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DisableCoachFlag"
responses:
"200":
description: Resolved next-state echoed back for optimistic-update consumers.
content:
application/json:
schema:
$ref: "#/components/schemas/PatchDisableCoachResponse"
"401": *a1
"422": *a3
"429": *a4
/api/auth/me/source-priority:
get:
tags:
- Auth
summary: Read per-user source priority
description: Returns the per-metric-class source priority used by the analytics aggregator. Missing keys fall back to
`DEFAULT_SOURCE_PRIORITY`. Null persisted state resolves to the documented defaults.
responses:
"200":
description: Resolved priority.
content:
application/json:
schema:
$ref: "#/components/schemas/GetSourcePriorityResponse"
"401": *a1
"422": *a3
"429": *a4
put:
tags:
- Auth
summary: Replace per-user source priority
description: Persists the supplied priority. Body is validated against `SourcePriority`; missing keys read as
`DEFAULT_SOURCE_PRIORITY` at the call site.
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
steps:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
activeEnergy:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
walkingRunningDistance:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
flightsClimbed:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
sleep:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
weight:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
bloodPressure:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
pulse:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
bodyFat:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
bodyTemperature:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
spo2:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
hrv:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
restingHeartRate:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
vo2Max:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
skinTemperature:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
respiratoryRate:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
recovery:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
metricPriority:
type: object
properties:
steps:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
activeEnergy:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
walkingRunningDistance:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
flightsClimbed:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
sleep:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
weight:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
bloodPressure:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
pulse:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
bodyFat:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
bodyTemperature:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
spo2:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
hrv:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
restingHeartRate:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
vo2Max:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
skinTemperature:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
respiratoryRate:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
recovery:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
deviceTypePriority:
type: object
properties:
default:
maxItems: 8
type: array
items:
type: string
enum:
- watch
- band
- ring
- phone
- scale
- other
- unknown
additionalProperties:
maxItems: 8
type: array
items:
type: string
enum:
- watch
- band
- ring
- phone
- scale
- other
- unknown
responses:
"200":
description: Saved priority (defaulted) echoed back.
content:
application/json:
schema:
$ref: "#/components/schemas/PutSourcePriorityResponse"
"401": *a1
"422": *a3
"429": *a4
/api/user/avatar:
post:
tags:
- Auth
summary: Upload the calling user's avatar
description: "Multipart upload (field `file`). Accepts image/jpeg, image/png, image/webp; rejects anything else at the
magic-byte sniff (the multipart Content-Type header is informational only). Hard caps: 2 MiB body, 2048×2048
dimensions. Replaces the v1.4.22 Gravatar leak by storing the bytes on the User row + serving them from
same-origin."
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
description: Binary image payload (JPEG / PNG / WebP). Hard-capped at 2 MiB and 2048×2048.
required:
- file
responses:
"201":
description: Avatar saved. `avatarUrl` is the same value the /me payload returns; the `?v=` suffix busts client caches
on re-upload.
content:
application/json:
schema:
$ref: "#/components/schemas/UploadAvatarResponse"
"401": *a1
"413":
description: Upload exceeds the 2 MiB byte limit or the 2048×2048 dimension limit.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"415":
description: Image format is not one of JPEG / PNG / WebP (magic-byte sniff).
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a3
"429": *a4
delete:
tags:
- Auth
summary: Clear the calling user's avatar
description: Resets the avatar columns to null. Idempotent — a delete on an already-empty row returns 204 with no
audit-log row.
responses:
"204":
description: Avatar cleared (or already empty).
"401": *a1
"422": *a3
"429": *a4
/api/user/avatar/{id}:
get:
tags:
- Auth
summary: Serve a user's avatar bytes
description: Owner-scoped. The `{id}` path segment must match the calling user's id — cross-user reads return 403.
Response body is the raw image bytes with the persisted `Content-Type`; the /me payload appends
`?v={updatedAtMs}` so clients can cache aggressively.
parameters:
- in: path
name: id
schema:
type: string
description: User id (must match the calling user).
required: true
description: User id (must match the calling user).
responses:
"200":
description: Avatar bytes.
content:
image/jpeg:
schema:
type: string
description: Raw JPEG bytes.
image/png:
schema:
type: string
description: Raw PNG bytes.
image/webp:
schema:
type: string
description: Raw WebP bytes.
"401": *a1
"403":
description: Caller is not the avatar's owner.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"404":
description: The user has no uploaded avatar.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a3
"429": *a4
/api/user/profile:
get:
tags:
- Auth
summary: Read the calling user's profile
description: Flattened profile fields for the native client. Aliased over the same data exposed by /api/auth/me. The
KVNR is decrypted server-side; the IKNR (v1.8.6) is returned plaintext.
responses:
"200":
description: The calling user's profile.
content:
application/json:
schema:
$ref: "#/components/schemas/GetProfileResponse"
"401": *a1
"422": *a3
"429": *a4
patch:
tags:
- Auth
summary: Update the calling user's profile
description: Partial profile update — every field is optional. An explicit null (or empty string) clears a field. A
non-empty `insurerIkNumber` must be exactly 9 digits (422 otherwise); a non-empty `insuranceNumber` (KVNR) must
pass the mod-10 check. `userId` is never accepted from the body.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ProfileUpdateRequest"
responses:
"200":
description: Saved profile echoed back.
content:
application/json:
schema:
$ref: "#/components/schemas/PatchProfileResponse"
"401": *a1
"422": *a3
"429": *a4
/api/insights/layout:
get:
tags:
- Insights
summary: Read the calling user's Insights tile layout
description: Returns the per-user Insights tile layout (visibility + order). Falls back to the default layout when the
user has not customised it. Mirrors the dashboard-widgets contract.
responses:
"200":
description: The resolved layout (custom or default).
content:
application/json:
schema:
$ref: "#/components/schemas/InsightsLayout"
"401": *a1
"422": *a3
"429": *a4
put:
tags:
- Insights
summary: Replace the calling user's Insights tile layout
description: Persists the full tile layout. The normalised layout is returned. Invalid bodies return the multi-issue 422
envelope, matching the dashboard-widgets route.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/InsightsLayoutBody"
responses:
"200":
description: Layout saved; the normalised layout is echoed back.
content:
application/json:
schema:
$ref: "#/components/schemas/InsightsLayoutSaved"
"401": *a1
"422": *a3
"429": *a4
delete:
tags:
- Insights
summary: Reset the calling user's Insights tile layout
description: Clears the persisted layout and returns the default layout. Idempotent.
responses:
"200":
description: Layout reset; the default layout is returned.
content:
application/json:
schema:
$ref: "#/components/schemas/InsightsLayoutReset"
"401": *a1
"422": *a3
"429": *a4
/api/insights/coach/facts:
get:
tags:
- Insights
summary: List the caller's durable Coach facts
description: v1.11.1 — returns the active facts the Coach has extracted about the caller (highest-confidence then newest
first), each decrypted on the fly. The GDPR 'what do you know about me' surface. Coach-gated
(`requireAssistantSurface("coach")`). Auth via cookie or Bearer; the owner is always narrowed from the session,
never the body. Undecryptable rows are omitted rather than failing the read.
responses:
"200":
description: The caller's active facts.
content:
application/json:
schema:
$ref: "#/components/schemas/CoachFactsList"
"401": *a1
"422": *a3
"429": *a4
delete:
tags:
- Insights
summary: Forget all of the caller's Coach facts
description: "v1.11.1 — bulk 'forget what you know about me': soft-deletes every active fact for the caller and returns
the count cleared. Idempotent (a second call clears 0). Coach-gated. Auth via cookie or Bearer."
responses:
"200":
description: All active facts cleared; the count is returned.
content:
application/json:
schema:
$ref: "#/components/schemas/CoachFactsCleared"
"401": *a1
"422": *a3
"429": *a4
/api/insights/coach/facts/{id}:
delete:
tags:
- Insights
summary: Forget one Coach fact
description: "v1.11.1 — soft-deletes a single fact owned by the caller. An unknown / cross-user / already-deleted id is
an idempotent no-op returning `{ deleted: false }`, never revealing whether the id exists under another account.
Coach-gated. Auth via cookie or Bearer."
responses:
"200":
description: "The fact was soft-deleted (`deleted: true`) or the id matched nothing the caller owns (`deleted: false`)."
content:
application/json:
schema:
$ref: "#/components/schemas/CoachFactDeleted"
"401": *a1
"422": *a3
"429": *a4
/api/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.
responses:
"200":
description: Insights bundle.
content:
application/json:
schema:
$ref: "#/components/schemas/InsightsComprehensiveResponseEnvelope"
"401": *a1
"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:<userId>`, one warm per 3 minutes) → 429 on a tight loop. Auth via cookie or Bearer; `userId` is
taken from the session, never the body.
requestBody:
required: false
content:
application/json:
schema:
$ref: "#/components/schemas/InsightsPregenerateRequest"
responses:
"200":
description: Warm accepted and enqueued. The work runs on the worker; poll the read-only status routes for the text.
content:
application/json:
schema:
$ref: "#/components/schemas/InsightsPregenerateResponseEnvelope"
"401": *a1
"422": *a3
"429": *a4
/api/analytics/range:
get:
tags:
- Analytics
summary: Single-metric period-over-period range delta
description: v1.9.0 — returns the current-window aggregate, the previous comparable window, and the composed delta for
ONE metric type over a `7d` / `30d` / `90d` / `1y` range. Single-type by construction (the metric page is
single-metric), so the read is one rollup-tier call covering the trailing 2N days sliced into the two halves —
no per-type fan-out. Additive route; the `/api/analytics` envelope is unchanged. Auth via cookie or Bearer.
parameters:
- in: query
name: type
schema:
type: string
enum:
- WEIGHT
- BLOOD_PRESSURE_SYS
- BLOOD_PRESSURE_DIA
- PULSE
- BODY_FAT
- SLEEP_DURATION
- ACTIVITY_STEPS
- BLOOD_GLUCOSE
- TOTAL_BODY_WATER
- BONE_MASS
- OXYGEN_SATURATION
- HEART_RATE_VARIABILITY
- RESTING_HEART_RATE
- ACTIVE_ENERGY_BURNED
- FLIGHTS_CLIMBED
- WALKING_RUNNING_DISTANCE
- VO2_MAX
- BODY_TEMPERATURE
- FAT_FREE_MASS
- FAT_MASS
- MUSCLE_MASS
- SKIN_TEMPERATURE
- PULSE_WAVE_VELOCITY
- VASCULAR_AGE
- VISCERAL_FAT
- AUDIO_EXPOSURE_ENV
- AUDIO_EXPOSURE_HEADPHONE
- TIME_IN_DAYLIGHT
- WALKING_STEADINESS
- AUDIO_EXPOSURE_EVENT
- RESPIRATORY_RATE
- BODY_MASS_INDEX
- LEAN_BODY_MASS
- WALKING_HEART_RATE_AVERAGE
- WALKING_ASYMMETRY
- WALKING_DOUBLE_SUPPORT
- WALKING_STEP_LENGTH
- WALKING_SPEED
- CARDIO_RECOVERY
- WRIST_TEMPERATURE
- FALL_COUNT
- SIX_MINUTE_WALK_DISTANCE
- STAIR_ASCENT_SPEED
- STAIR_DESCENT_SPEED
- BREATHING_DISTURBANCES
- IRREGULAR_RHYTHM_NOTIFICATION
- HIGH_HEART_RATE_EVENT
- LOW_HEART_RATE_EVENT
- WALKING_STEADINESS_EVENT
- BREATHING_DISTURBANCE_EVENT
- RECOVERY_SCORE
- STRESS_SCORE
- STRAIN_SCORE
- HRV_RMSSD
- DAY_STRAIN
- WORKOUT_STRAIN
- SLEEP_PERFORMANCE
- SLEEP_EFFICIENCY
- SLEEP_CONSISTENCY
- SLEEP_NEED
- ENERGY_EXPENDITURE_KJ
- AVERAGE_HEART_RATE
- MAX_HEART_RATE
- SLEEP_DISTURBANCE_COUNT
description: "The measurement type to read (single metric — no fan-out). Closed enum: an unknown type 422s."
required: true
description: "The measurement type to read (single metric — no fan-out). Closed enum: an unknown type 422s."
- in: query
name: range
schema:
type: string
enum:
- 7d
- 30d
- 90d
- 1y
description: "Trailing window: `7d` / `30d` / `90d` / `1y`. The previous comparable window is the equally-sized span
immediately before it."
required: true
description: "Trailing window: `7d` / `30d` / `90d` / `1y`. The previous comparable window is the equally-sized span
immediately before it."
responses:
"200":
description: Current + previous window aggregates and the delta.
content:
application/json:
schema:
$ref: "#/components/schemas/AnalyticsRangeResponseEnvelope"
"401": *a1
"422": *a3
"429": *a4
/api/insights/blood-pressure-status:
get:
tags:
- Insights
summary: Blood-pressure assessment
description: "Data-driven plain-language assessment of the user's recent blood-pressure readings. Read-only: a cache
miss warms a generation out of band and serves the last-good text meanwhile (stale-while-revalidate). Auth via
cookie or Bearer."
parameters:
- in: query
name: locale
schema:
description: Optional UI-locale override; defaults to the session locale.
type: string
enum:
- de
- en
description: Optional UI-locale override; defaults to the session locale.
responses:
"200":
description: Assessment envelope (fresh, cached, or preparing).
content:
application/json:
schema:
$ref: "#/components/schemas/BloodPressureStatusResponseEnvelope"
"401": *a1
"422": *a3
"429": *a4
/api/insights/pulse-status:
get:
tags:
- Insights
summary: Pulse assessment
description: "Data-driven plain-language assessment of the user's recent resting-pulse readings. Read-only: a cache miss
warms a generation out of band and serves the last-good text meanwhile (stale-while-revalidate). Auth via cookie
or Bearer."
parameters:
- in: query
name: locale
schema:
description: Optional UI-locale override; defaults to the session locale.
type: string
enum:
- de
- en
description: Optional UI-locale override; defaults to the session locale.
responses:
"200":
description: Assessment envelope (fresh, cached, or preparing).
content:
application/json:
schema:
$ref: "#/components/schemas/PulseStatusResponseEnvelope"
"401": *a1
"422": *a3
"429": *a4
/api/insights/weight-status:
get:
tags:
- Insights
summary: Weight assessment
description: "Data-driven plain-language assessment of the user's recent weight trend. Read-only: a cache miss warms a
generation out of band and serves the last-good text meanwhile (stale-while-revalidate). Auth via cookie or
Bearer."
parameters:
- in: query
name: locale
schema:
description: Optional UI-locale override; defaults to the session locale.
type: string
enum:
- de
- en
description: Optional UI-locale override; defaults to the session locale.
responses:
"200":
description: Assessment envelope (fresh, cached, or preparing).
content:
application/json:
schema:
$ref: "#/components/schemas/WeightStatusResponseEnvelope"
"401": *a1
"422": *a3
"429": *a4
/api/insights/bmi-status:
get:
tags:
- Insights
summary: BMI assessment
description: "Data-driven plain-language assessment of the user's body-mass index. Read-only: a cache miss warms a
generation out of band and serves the last-good text meanwhile (stale-while-revalidate). Auth via cookie or
Bearer."
parameters:
- in: query
name: locale
schema:
description: Optional UI-locale override; defaults to the session locale.
type: string
enum:
- de
- en
description: Optional UI-locale override; defaults to the session locale.
responses:
"200":
description: Assessment envelope (fresh, cached, or preparing).
content:
application/json:
schema:
$ref: "#/components/schemas/BmiStatusResponseEnvelope"
"401": *a1
"422": *a3
"429": *a4
/api/insights/mood-status:
get:
tags:
- Insights
summary: Mood assessment
description: "Data-driven plain-language assessment of the user's recent mood entries. Read-only: a cache miss warms a
generation out of band and serves the last-good text meanwhile (stale-while-revalidate). Auth via cookie or
Bearer."
parameters:
- in: query
name: locale
schema:
description: Optional UI-locale override; defaults to the session locale.
type: string
enum:
- de
- en
description: Optional UI-locale override; defaults to the session locale.
responses:
"200":
description: Assessment envelope (fresh, cached, or preparing).
content:
application/json:
schema:
$ref: "#/components/schemas/MoodStatusResponseEnvelope"
"401": *a1
"422": *a3
"429": *a4
/api/insights/medication-compliance-status:
get:
tags:
- Insights
summary: Medication-compliance assessment
description: "Data-driven plain-language assessment of the user's medication compliance — an overall `summary` plus a
per-medication note array. Read-only: a cache miss warms a generation out of band and serves the last-good
envelope meanwhile (stale-while-revalidate). Auth via cookie or Bearer."
parameters:
- in: query
name: locale
schema:
description: Optional UI-locale override; defaults to the session locale.
type: string
enum:
- de
- en
description: Optional UI-locale override; defaults to the session locale.
responses:
"200":
description: Compliance assessment envelope (fresh, cached, or preparing).
content:
application/json:
schema:
$ref: "#/components/schemas/MedicationComplianceStatusResponseEnvelope"
"401": *a1
"422": *a3
"429": *a4
/api/insights/metric-status:
get:
tags:
- Insights
summary: Generic per-HealthKit-metric assessment
description: "v1.8.7.1 — data-driven plain-language assessment for any registered HealthKit metric (resting heart rate,
sleep, glucose, body composition, gait, audio exposure, …). One generic route covering ~30 metric pages via
archetype prompt templates + per-metric metadata. Read-only: a cache miss warms a generation out of band and
serves the last-good text meanwhile (stale-while-revalidate). An unknown `metric` 422s against the closed
registry enum. Auth via cookie or Bearer."
parameters:
- in: query
name: metric
schema:
type: string
enum:
- RESTING_HEART_RATE
- HEART_RATE_VARIABILITY
- OXYGEN_SATURATION
- RESPIRATORY_RATE
- BODY_TEMPERATURE
- SKIN_TEMPERATURE
- BLOOD_GLUCOSE
- WALKING_HEART_RATE_AVERAGE
- PULSE_WAVE_VELOCITY
- VASCULAR_AGE
- STEPS
- ACTIVE_ENERGY
- FLIGHTS_CLIMBED
- WALKING_RUNNING_DISTANCE
- TIME_IN_DAYLIGHT
- VO2_MAX
- TOTAL_BODY_WATER
- BONE_MASS
- FAT_FREE_MASS
- FAT_MASS
- MUSCLE_MASS
- LEAN_BODY_MASS
- VISCERAL_FAT
- WALKING_STEADINESS
- WALKING_ASYMMETRY
- WALKING_DOUBLE_SUPPORT
- WALKING_STEP_LENGTH
- WALKING_SPEED
- AUDIO_EXPOSURE_ENV
- AUDIO_EXPOSURE_HEADPHONE
- AUDIO_EXPOSURE_EVENT
- SLEEP_DURATION
- CARDIO_RECOVERY
- WRIST_TEMPERATURE
- FALL_COUNT
- SIX_MINUTE_WALK_DISTANCE
- STAIR_ASCENT_SPEED
- STAIR_DESCENT_SPEED
- BREATHING_DISTURBANCES
description: "HealthKit metric id to assess (e.g. RESTING_HEART_RATE, SLEEP_DURATION). Closed enum: an unknown id 422s.
The seven specialised metrics are served by their own routes and are not accepted here."
required: true
description: "HealthKit metric id to assess (e.g. RESTING_HEART_RATE, SLEEP_DURATION). Closed enum: an unknown id 422s.
The seven specialised metrics are served by their own routes and are not accepted here."
- in: query
name: locale
schema:
description: Optional UI-locale override; defaults to the session locale.
type: string
enum:
- de
- en
description: Optional UI-locale override; defaults to the session locale.
responses:
"200":
description: Assessment envelope (fresh, cached, or preparing).
content:
application/json:
schema:
$ref: "#/components/schemas/MetricStatusResponseEnvelope"
"401": *a1
"422": *a3
"429": *a4
/api/insights/derived:
get:
tags:
- Insights
summary: Derived wellness metric (compute-once)
description: v1.10.0 — the compute-once `Derived<T>` 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<T>` union so the native client can decode one stable shape and combine
values across metrics. Auth via cookie or Bearer.
parameters:
- in: query
name: metric
schema:
type: string
enum:
- VITALS_BASELINE
- FITNESS_AGE
- VASCULAR_AGE_DELTA
- SLEEP_SCORE
- READINESS
- COINCIDENT_DEVIATION
- HRV_BALANCE
- BMI
- RECOVERY_SCORE
- STRESS_SCORE
- STRAIN_SCORE
- WRIST_TEMPERATURE_BASELINE
- STAIR_ASCENT_SPEED_BASELINE
- STAIR_DESCENT_SPEED_BASELINE
- SIX_MINUTE_WALK_BAND
- TRAJECTORY
description: "Derived-metric id to compute (e.g. VITALS_BASELINE, FITNESS_AGE, VASCULAR_AGE_DELTA, HRV_BALANCE, BMI,
READINESS). Closed enum: an unknown id 422s. Metrics whose compute has not yet landed return an
`insufficient` value with reason `not_implemented`."
required: true
description: "Derived-metric id to compute (e.g. VITALS_BASELINE, FITNESS_AGE, VASCULAR_AGE_DELTA, HRV_BALANCE, BMI,
READINESS). Closed enum: an unknown id 422s. Metrics whose compute has not yet landed return an
`insufficient` value with reason `not_implemented`."
- in: query
name: type
schema:
description: For VITALS_BASELINE only — the single vital to band (defaults to RESTING_HEART_RATE). Ignored by
composites. An unsupported value yields an `insufficient` value rather than a 422 so iOS metric
combinations stay forgiving.
type: string
enum:
- RESTING_HEART_RATE
- HEART_RATE_VARIABILITY
- RESPIRATORY_RATE
- OXYGEN_SATURATION
- BODY_TEMPERATURE
- SKIN_TEMPERATURE
- BLOOD_PRESSURE_SYS
- BLOOD_PRESSURE_DIA
- BLOOD_GLUCOSE
- PULSE
- WEIGHT
description: For VITALS_BASELINE only — the single vital to band (defaults to RESTING_HEART_RATE). Ignored by
composites. An unsupported value yields an `insufficient` value rather than a 422 so iOS metric combinations
stay forgiving.
responses:
"200":
description: The flat derived-metric value (ok or insufficient).
content:
application/json:
schema:
$ref: "#/components/schemas/DerivedMetricResponseEnvelope"
"401": *a1
"422": *a3
"429": *a4
/api/insights/derived/batch:
get:
tags:
- Insights
summary: Derived wellness metrics (batched compute-once)
description: v1.10.0 — resolve several derived wellness metrics in ONE request. The `metrics` CSV names the metrics (a
`metric:type` token sub-targets a VITALS_BASELINE vital); the server fans out under a bounded limiter with the
profile loaded once and returns a map keyed by the per-request token. Collapses the Insights cold-mount fan-out
of 14+ independent single-metric requests — the pool-starvation class that surfaces as a hang-then-recover. The
single-metric route stays for the per-score detail pages. Auth via cookie or Bearer.
parameters:
- in: query
name: metrics
schema:
type: string
minLength: 1
maxLength: 1024
description: Comma-separated derived-metric tokens. Each is a `<DERIVED_METRIC_ID>` or
`<DERIVED_METRIC_ID>:<MeasurementType>` (the colon sub-targets a VITALS_BASELINE vital). An unknown id
422s; at most 24 tokens; duplicates collapse.
required: true
description: Comma-separated derived-metric tokens. Each is a `<DERIVED_METRIC_ID>` or
`<DERIVED_METRIC_ID>:<MeasurementType>` (the colon sub-targets a VITALS_BASELINE vital). An unknown id 422s;
at most 24 tokens; duplicates collapse.
responses:
"200":
description: The map of derived-metric values, keyed by token.
content:
application/json:
schema:
$ref: "#/components/schemas/DerivedBatchResponseEnvelope"
"401": *a1
"422": *a3
"429": *a4
/api/insights/correlations:
get:
tags:
- Insights
summary: Correlation discovery (FDR-controlled)
description: v1.10.0 — scans a curated behaviour × outcome matrix (daylight / mood / glucose / BP / steps × sleep / HRV
/ resting HR / weight), lag-joins each behaviour day to the next day's outcome, runs Pearson with the exact
Student-t p-value, and applies Benjamini-Hochberg FDR control across every tested pair. Only
statistically-defensible pairs surface, each carrying n, r, p, and the BH-adjusted q. Descriptive, never causal.
Gated by the operator `correlations` assistant surface. Auth via cookie or Bearer.
responses:
"200":
description: The discovered correlations + the tested-pair count.
content:
application/json:
schema:
$ref: "#/components/schemas/CorrelationDiscoveryResponseEnvelope"
"401": *a1
"422": *a3
"429": *a4
components:
schemas:
PasskeyLoginVerifyRequest:
type: object
properties:
challengeId:
type: string
minLength: 1
description: Server-issued WebAuthn challenge id.
credential:
type: object
properties:
id:
type: string
rawId:
type: string
type:
type: string
const: public-key
response:
type: object
propertyNames:
type: string
additionalProperties: {}
authenticatorAttachment:
type: string
enum:
- platform
- cross-platform
clientExtensionResults:
type: object
propertyNames:
type: string
additionalProperties: {}
required:
- id
- rawId
- type
- response
description: SimpleWebAuthn-style assertion response payload.
required:
- challengeId
- credential
description: Passkey assertion verification. Same native-client token issuance as the password path.
RefreshTokenRequest:
type: object
properties:
refreshToken:
type: string
minLength: 1
description: Caller-presented refresh token (`hlr_<64hex>`).
revoke:
description: When true, revoke the supplied token instead of rotating.
type: boolean
required:
- refreshToken
description: Exchange a one-time-use refresh token for a fresh access + refresh pair.
AppleHealthBatchRequest:
type: object
properties:
entries:
minItems: 1
maxItems: 500
type: array
items:
$ref: "#/components/schemas/AppleHealthBatchEntry"
required:
- entries
description: Apple Health batch ingest. ≤500 entries per call; idempotent via `Idempotency-Key`.
AppleHealthBatchEntry:
type: object
properties:
hkIdentifier:
type: string
minLength: 1
maxLength: 120
description: HealthKit identifier (e.g. `HKQuantityTypeIdentifierBodyMass`).
value:
type: number
unit:
type: string
minLength: 1
maxLength: 60
startDate:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
endDate:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
sleepStage:
description: HKCategoryValueSleepAnalysis codepoint; only for sleep samples.
type: integer
minimum: 0
maximum: 20
categoryValue:
description: v1.10.0 — `HKCategoryValue` codepoint for an EVENT-class category sample (irregular-rhythm, high/low-HR,
walking-steadiness, breathing-disturbance). Carries the device's own classification verdict / severity,
which the server resolves to a stored `rhythmClassification`. HealthLog stores ONLY the device's result — it
never re-classifies. Ignored for non-event identifiers.
type: integer
minimum: 0
maximum: 20
externalId:
type: string
minLength: 1
maxLength: 120
description: HKSample.uuid string — the dedup key.
externalSourceVersion:
type: string
minLength: 1
maxLength: 120
source:
description: Source attribution for the row. Defaults to `APPLE_HEALTH` when omitted. Send `MANUAL` to tag rows the user
entered by hand on-device (e.g. the standalone adopt-on-pair backfill). Part of the `(userId, type, source,
externalId)` dedup key, so the same externalId under a different source is a distinct row. `WITHINGS` /
`IMPORT` are not accepted here.
type: string
enum:
- APPLE_HEALTH
- MANUAL
deviceType:
description: Device class mapped from `HKDevice.model`. Used by the analytics aggregator to break ties when the same
source contributed multiple devices for the same day. Omit (or send null) on legacy clients — the server
treats it as `unknown` and the picker falls through.
anyOf:
- type: string
enum:
- watch
- band
- ring
- phone
- scale
- other
- unknown
- type: "null"
required:
- hkIdentifier
- value
- unit
- startDate
- endDate
- externalId
MeasurementsDeleteByExternalIdsRequest:
type: object
properties:
externalIds:
minItems: 0
maxItems: 500
type: array
items:
type: string
minLength: 1
maxLength: 120
required:
- externalIds
description: iOS deletion-sync. Up to 500 externalIds per call; matching rows owned by other users are silently skipped
(cross-user 404 guard).
DeviceRegisterRequest:
type: object
properties:
token:
type: string
minLength: 8
maxLength: 512
pattern: ^[A-Za-z0-9+/=._:-]+$
description: Generic device identifier (legacy; APNs token below).
bundleId:
type: string
minLength: 1
maxLength: 128
locale:
type: string
minLength: 2
maxLength: 16
appVersion:
type: string
minLength: 1
maxLength: 32
model:
type: string
minLength: 1
maxLength: 64
apnsToken:
description: Hex-encoded APNs device token. Must be paired with `apnsEnvironment`.
type: string
minLength: 8
maxLength: 256
pattern: ^[A-Fa-f0-9]+$
apnsEnvironment:
description: Gateway the iOS client received `apnsToken` from. Server never auto-detects.
type: string
enum:
- sandbox
- production
medicationDelivery:
description: v1.7.0 per-device medication-delivery override. NULL / omitted = inherit the user-level roaming default.
"server" forces server APNs for this device; "client" forces local. Stored + echoed; cron suppression stays
user-level.
anyOf:
- type: string
enum:
- server
- client
- type: "null"
required:
- token
- bundleId
description: Native device registration. Re-registering an APNs token belonging to another user returns 409
(cross-user-hijack guard).
CreateMedicationRequest:
type: object
properties:
name:
type: string
minLength: 1
maxLength: 100
dose:
type: string
minLength: 1
maxLength: 50
category:
type: string
enum:
- BLOOD_PRESSURE
- VITAMIN
- SUPPLEMENT
- PAIN_RELIEF
- ALLERGY
- DIGESTIVE
- THYROID
- HORMONE
- SKIN
- SLEEP_AID
- DIABETES
- ANTIBIOTIC
- OTHER
treatmentClass:
type: string
enum:
- GENERIC
- GLP1
dosesPerUnit:
type: integer
minimum: 1
maximum: 100
deliveryForm:
type: string
enum:
- ORAL
- INJECTION
- OTHER
trackInjectionSites:
description: Per-medication injection-site tracking opt-in. Default false. Only meaningful for an INJECTION delivery
form; when true the client prompts (skippably) for the site after a taken dose.
type: boolean
allowedInjectionSites:
description: Per-medication allowed / preferred injection sites. Empty array = no per-medication restriction (every site
offered). The effective pickable set is this list minus the user's global exclusion.
maxItems: 8
type: array
items:
type: string
enum:
- ABDOMEN_LEFT
- ABDOMEN_RIGHT
- ABDOMEN_UPPER_LEFT
- ABDOMEN_UPPER_RIGHT
- THIGH_LEFT
- THIGH_RIGHT
- UPPER_ARM_LEFT
- UPPER_ARM_RIGHT
notificationsEnabled:
type: boolean
liveActivityEnabled:
description: iOS Live Activity opt-in for this medication's reminders. Default false. The iOS client owns the
ActivityKit lifecycle; the server only stores + echoes the flag.
type: boolean
criticalAlarmEnabled:
description: iOS 26 AlarmKit critical-reminder opt-in. Default false. Critical alarms bypass the device mute switch /
Focus; the server stores the preference only and hangs no server-side behaviour off it.
type: boolean
atcCode:
description: Optional WHO ATC classification code (active-substance class, 7 chars, e.g. `A10BX10`).
User/clinician-asserted; never machine-guessed. Emitted on the FHIR `medicationCodeableConcept` under
`http://www.whocc.no/atc`. NULL clears it; absent leaves it untouched.
anyOf:
- type: string
pattern: ^[A-Z]\d{2}[A-Z]{2}\d{2}$
- type: "null"
rxNormCode:
description: Optional RxNorm RxCUI (numeric, US identifier, e.g. `2601723`). Secondary coding emitted under
`http://www.nlm.nih.gov/research/umls/rxnorm` alongside any ATC code, never instead of the free-text name.
NULL clears it; absent leaves it untouched.
anyOf:
- type: string
maxLength: 20
pattern: ^\d+$
- type: "null"
startsOn:
description: Date the medication course begins (ISO `YYYY-MM-DD`). Anchors RRULE BYDAY / BYMONTHDAY patterns and the
rolling-interval countdown's first window. NULL = active from creation (the legacy implicit behaviour).
Required when `oneShot` is true.
anyOf:
- type: string
format: date
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))$
- type: "null"
endsOn:
description: Date the medication course ends (ISO `YYYY-MM-DD`). NULL = no end date (chronic). When `oneShot` is true
the route normalises `endsOn` to equal `startsOn`.
anyOf:
- type: string
format: date
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))$
- type: "null"
oneShot:
description: Single-administration medication (e.g. flu shot, post-op single dose). When true the medication has at most
one schedule with no `rrule` / `rollingIntervalDays`, and `active` auto-flips to false once the dose is
logged (non-skipped).
type: boolean
schedules:
minItems: 1
type: array
items:
$ref: "#/components/schemas/MedicationScheduleInput"
required:
- name
- dose
- schedules
description: 'Create-medication body. The route enforces the v1.5 cross-field invariants on top of the per-schedule
`rrule_xor_rolling` Zod refine: a `oneShot:true` medication may carry at most one schedule and that schedule
must not declare a recurrence; `endsOn` is normalised to equal `startsOn` for one-shot doses; a recurring
schedule with no `rrule`, `rollingIntervalDays`, or legacy `daysOfWeek` defaults to `rrule = "FREQ=DAILY"`; and
`timesOfDay` is dual-written from `windowStart` when the caller omits it.'
MedicationScheduleInput:
type: object
properties:
windowStart:
type: string
pattern: ^([01]\d|2[0-3]):([0-5]\d)$
description: Legacy single-time-of-intake (HH:mm, user local). v1.5 keeps the field for backwards compatibility with
pre-wizard iOS clients; the new `timesOfDay` array supersedes it.
windowEnd:
type: string
pattern: ^([01]\d|2[0-3]):([0-5]\d)$
description: Legacy reminder-window upper bound (HH:mm). Used to derive the late-classification grace span when
`reminderGraceMinutes` is null.
label:
description: Optional human label (e.g. "Morning", "Evening").
type: string
maxLength: 50
dose:
description: Per-schedule dose override. NULL means the schedule inherits `Medication.dose`.
type: string
maxLength: 50
daysOfWeek:
description: Legacy day-of-week filter (0=Sunday..6=Saturday). v1.5 reads new writes through `rrule` first; this field
is preserved for pre-v1.5 rows and is the input the route serialises into the persisted `days_of_week`
string.
type: array
items:
type: integer
minimum: 0
maximum: 6
intervalWeeks:
description: Legacy multi-week stride (1..4). Bi-weekly + tri-weekly were broken in the pre-v1.5 reminder worker; new
writes encode the same intent via `rrule` (e.g. `FREQ=WEEKLY;INTERVAL=2;BYDAY=WE`).
type: integer
minimum: 1
maximum: 4
timesOfDay:
description: v1.5 first-class points-in-time the dose is taken (HH:mm, user local). Up to 8 entries. Absent or empty
means the route stamps `[windowStart]` so the new engine always sees a populated array.
maxItems: 8
type: array
items:
type: string
pattern: ^([01]\d|2[0-3]):([0-5]\d)$
reminderGraceMinutes:
description: Reminder grace window in minutes. Caps at 24h. NULL falls back to the legacy `windowEnd - windowStart` span.
type: integer
minimum: 1
maximum: 1440
rrule:
description: "RFC 5545 RRULE string (subset). Use for daily / weekly-with-BYDAY / multi-week / monthly / yearly
cadences. Mutually exclusive with `rollingIntervalDays`. Examples: `FREQ=DAILY`,
`FREQ=WEEKLY;BYDAY=MO,WE,FR`, `FREQ=WEEKLY;INTERVAL=2;BYDAY=WE`, `FREQ=MONTHLY;BYMONTHDAY=1`,
`FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=1`."
type: string
maxLength: 200
pattern: ^FREQ=(?:DAILY|WEEKLY|MONTHLY|YEARLY)(?:;(?:INTERVAL=\d{1,3}|BYDAY=(?:MO|TU|WE|TH|FR|SA|SU)(?:,(?:MO|TU|WE|TH|FR|SA|SU))*|BYMONTHDAY=-?\d{1,2}(?:,-?\d{1,2})*|BYMONTH=\d{1,2}(?:,\d{1,2})*|COUNT=\d{1,4}|UNTIL=\d{8}T\d{6}Z))*$
rollingIntervalDays:
description: Flexible-rolling interval in days, counted forward from the latest `MedicationIntakeEvent.takenAt` (the
dose re-anchors when logged). Mutually exclusive with `rrule`. Range 1..365.
type: integer
minimum: 1
maximum: 365
scheduleType:
description: Schedule type. SCHEDULED (default) = rrule / rolling / legacy cadence. PRN = as-needed (never projected,
reminded, or counted in compliance expected; still loggable via the intake route). CYCLIC = N weeks on / M
weeks off, gating whichever inner cadence the rrule / legacy fields describe.
type: string
enum:
- SCHEDULED
- PRN
- CYCLIC
cyclicOnWeeks:
description: Cyclic "on" weeks (1..52). Required when `scheduleType` is CYCLIC; ignored otherwise.
type: integer
minimum: 1
maximum: 52
cyclicOffWeeks:
description: Cyclic "off" weeks (0..52). Required when `scheduleType` is CYCLIC; ignored otherwise.
type: integer
minimum: 0
maximum: 52
required:
- windowStart
- windowEnd
description: Single schedule entry on a medication. v1.5 introduces `timesOfDay`, `rrule`, `rollingIntervalDays`, and
`reminderGraceMinutes` as first-class fields; `windowStart`, `windowEnd`, `daysOfWeek`, and `intervalWeeks` are
preserved through the v1.5.x line for backwards compatibility. **`rrule` and `rollingIntervalDays` are mutually
exclusive** — supplying both fails 422 (`rrule_xor_rolling`). The DB enforces the same invariant via a CHECK
constraint.
UpdateMedicationRequest:
type: object
properties:
name:
type: string
minLength: 1
maxLength: 100
dose:
type: string
minLength: 1
maxLength: 50
category:
type: string
enum:
- BLOOD_PRESSURE
- VITAMIN
- SUPPLEMENT
- PAIN_RELIEF
- ALLERGY
- DIGESTIVE
- THYROID
- HORMONE
- SKIN
- SLEEP_AID
- DIABETES
- ANTIBIOTIC
- OTHER
treatmentClass:
type: string
enum:
- GENERIC
- GLP1
dosesPerUnit:
anyOf:
- type: integer
minimum: 1
maximum: 100
- type: "null"
deliveryForm:
type: string
enum:
- ORAL
- INJECTION
- OTHER
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
schedules:
type: array
items:
$ref: "#/components/schemas/MedicationScheduleInput"
reminderGraceMinutes:
description: "Detail-page bridge: primary-schedule reminder-window in minutes. The route maps the value onto the primary
schedule's `reminderGraceMinutes` field; ignored when a full `schedules` array is also supplied."
anyOf:
- type: integer
minimum: 0
maximum: 1440
- type: "null"
description: Update-medication body. Every field is optional; omitted fields are left untouched. Supplying `schedules`
REPLACES the medication's full schedule list (the route deletes existing rows before re-creating). Flipping
`active` to false records the current timestamp on `pausedAt`; flipping back to true clears it. v1.5 invariants
on the `schedules` array match the create path.
MedicationIntakeRequest:
type: object
properties:
medicationId:
type: string
minLength: 1
description: Server-narrowed from the URL path. The route layer overwrites whatever the body supplies before Zod parsing
so a caller cannot log an intake against another medication.
scheduledFor:
description: Slot the dose belongs to. Defaults to `takenAt` (or `now()` when both are absent) so the compliance pairing
logic can pin the dose to a schedule slot.
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
takenAt:
description: When the dose was actually taken. NULL when `skipped` is true; defaults to `now()` for non-skipped intakes.
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
skipped:
default: false
description: True to log a skipped slot (no consumption, no inventory decrement, one-shot medications stay active).
type: boolean
idempotencyKey:
description: Caller-issued de-dup key. A second POST with the same key returns the original event without creating a new
row.
type: string
maxLength: 128
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
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).
MedicationExtractRequest:
type: object
properties:
text:
type: string
minLength: 1
maxLength: 2000
description: Free-text medication description (any locale). Up to 2 000 characters. The model never echoes the text back
into another tenant — it is only used to produce the structured fields.
locale:
description: Optional UI locale hint for the model.
type: string
enum:
- en
- de
- es
- fr
- it
- pl
today:
description: "Optional override of the reference date used to resolve relative phrases (\"tomorrow\", \"next Monday\").
Format: `YYYY-MM-DD`. Defaults to the server's UTC day."
type: string
pattern: ^\d{4}-\d{2}-\d{2}$
required:
- text
description: Free-text medication description payload. The route runs the text through the Coach provider chain and
returns a partial structured payload the wizard merges. Rate-limited 10 requests / 5 minutes / user;
budget-gated against the daily Coach token ceiling.
DisableCoachFlag:
type: object
properties:
disableCoach:
type: boolean
required:
- disableCoach
description: Per-account Coach opt-out toggle (v1.4.47 W3). `true` hides the Coach FAB and short-circuits its API gates.
ProfileUpdateRequest:
type: object
properties:
email:
anyOf:
- type: string
format: email
pattern: ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$
- type: "null"
heightCm:
anyOf:
- type: number
minimum: 50
maximum: 300
- type: "null"
dateOfBirth:
anyOf:
- type: string
- type: "null"
gender:
anyOf:
- type: string
enum:
- MALE
- FEMALE
- type: "null"
displayName:
anyOf:
- type: string
minLength: 1
maxLength: 80
- type: "null"
locale:
anyOf:
- type: string
enum:
- de
- en
- type: "null"
timezone:
type: string
minLength: 1
maxLength: 64
moodReminderEnabled:
type: boolean
fullName:
anyOf:
- type: string
maxLength: 120
- type: "null"
insurerName:
anyOf:
- type: string
maxLength: 120
- type: "null"
insuranceNumber:
description: German KVNR. Empty/null clears it; mod-10 check enforced.
anyOf:
- type: string
- type: "null"
insurerIkNumber:
description: German insurer institution number (IKNR). Optional; empty/null clears it. A non-empty value must be exactly
9 digits (no checksum enforced).
anyOf:
- type: string
- type: "null"
description: Partial profile update. Every field is optional; an omitted field is left untouched, an explicit null (or
empty string) clears it. `userId` is never accepted — it is narrowed from the session/token.
InsightsLayoutBody:
type: object
properties:
version:
type: number
const: 1
tiles:
minItems: 1
maxItems: 50
type: array
items:
type: object
properties:
id:
type: string
enum:
- overview
- blood-pressure
- pulse
- oxygen
- body-temperature
- respiratory-rate
- weight
- bmi
- body-water
- bone-mass
- fat-free-mass
- fat-mass
- muscle-mass
- visceral-fat
- lean-body-mass
- steps
- active-energy
- workouts
- flights-climbed
- walking-distance
- walking-steadiness
- walking-heart-rate
- walking-asymmetry
- double-support-time
- step-length
- walking-speed
- cardio-fitness
- falls
- six-minute-walk
- stair-ascent-speed
- stair-descent-speed
- sleep
- breathing-disturbances
- resting-pulse
- hrv
- pulse-wave-velocity
- vascular-age
- cardio-recovery
- environmental-audio
- headphone-audio
- audio-events
- daylight
- blood-glucose
- skin-temperature
- wrist-temperature
- mood
- medications
- blutdruck
- puls
- sauerstoff
- koerpertemperatur
- gewicht
- aktive-energie
- schlaf
- ruhepuls
- stimmung
- medikamente
visible:
type: boolean
order:
type: integer
minimum: 0
maximum: 99
required:
- id
- visible
- order
required:
- version
- tiles
description: "Per-user Insights tile layout: an ordered list of tiles with a visibility flag. `version` is the layout
schema version. Tile ids are a closed enum: the canonical ids are English (matching the routed
`/insights/<slug>` sub-pages). The legacy German ids (blutdruck, puls, sauerstoff, koerpertemperatur, gewicht,
aktive-energie, schlaf, ruhepuls, stimmung, medikamente) remain accepted on input for backward compatibility and
are normalised to their English equivalents before persisting; GET responses always carry the canonical English
ids. The legacy ids are deprecated and will be removed in a future major version."
CoachMessageFeedbackRequest:
type: object
properties:
rating:
type: string
enum:
- helpful
- unhelpful
reason:
type: string
minLength: 1
maxLength: 200
required:
- rating
description: Per-message helpful/unhelpful feedback (v1.4.23 H7). Optional `reason` is free-form prose, capped at 200 chars.
InsightsPregenerateRequest:
type: object
properties: {}
description: No body fields. The user is taken from the session / Bearer and the locale from the session; the warm
covers every assessment for that user.
MedicationTreatmentClass:
type: string
enum:
- GENERIC
- GLP1
description: Prisma-level treatment-class discriminator. `GLP1` unlocks the GLP-1 specialist surfaces (injection-site
rotation, titration history, pen inventory, GLP-1-aware Coach).
MedicationSchedule:
type: object
properties:
id:
type: string
medicationId:
type: string
windowStart:
type: string
description: Legacy single-time-of-intake (HH:mm, user local). Preserved for backwards compatibility; the new
`timesOfDay` array supersedes it.
windowEnd:
type: string
description: Legacy reminder-window upper bound (HH:mm). Used to derive the late-classification grace span when
`reminderGraceMinutes` is null.
label:
anyOf:
- type: string
- type: "null"
dose:
anyOf:
- type: string
- type: "null"
description: Per-schedule dose override. NULL means the schedule inherits `Medication.dose`.
daysOfWeek:
anyOf:
- type: string
- type: "null"
description: Legacy persisted recurrence encoding (`null` | `1,3,5` | `i2;1,3,5`). v1.5 readers consult `rrule` first;
the field is kept for pre-v1.5 rows. v1.6.0 drops the column.
timesOfDay:
type: array
items:
type: string
description: v1.5 first-class points-in-time the dose is taken (HH:mm, user local). Backfilled to `[windowStart]` for
every pre-v1.5 row.
reminderGraceMinutes:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
description: Reminder grace window in minutes. NULL falls back to the legacy `windowEnd - windowStart` span.
rrule:
anyOf:
- type: string
- type: "null"
description: RFC 5545 RRULE string (subset). Used for calendar-anchored cadences. **Mutually exclusive with
`rollingIntervalDays`** — exactly one of the two is non-null on any v1.5+ schedule (or both are null on
legacy rows that haven't been touched since the migration).
rollingIntervalDays:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
description: Flexible-rolling interval in days, counted forward from the latest `MedicationIntakeEvent.takenAt`.
**Mutually exclusive with `rrule`.**
scheduleType:
type: string
enum:
- SCHEDULED
- PRN
- CYCLIC
description: v1.7.0 schedule-type discriminator. SCHEDULED = rrule / rolling / legacy cadence. PRN = as-needed (never
projected, reminded, or counted in compliance expected; still loggable via the intake route). CYCLIC = N
weeks on / M weeks off, gating whichever inner cadence the rrule / legacy fields describe.
cyclicOnWeeks:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
description: v1.7.0 cyclic "on" weeks. Only meaningful when `scheduleType` is CYCLIC; null otherwise.
cyclicOffWeeks:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
description: v1.7.0 cyclic "off" weeks. Only meaningful when `scheduleType` is CYCLIC; null otherwise.
required:
- id
- medicationId
- windowStart
- windowEnd
- label
- dose
- daysOfWeek
- timesOfDay
- reminderGraceMinutes
- rrule
- rollingIntervalDays
- scheduleType
- cyclicOnWeeks
- cyclicOffWeeks
description: Schedule entry attached to a medication. v1.5 promotes `timesOfDay` to first-class and introduces `rrule`
(calendar-anchored cadences) and `rollingIntervalDays` (flexible-rolling cadences). The two recurrence
primitives are mutually exclusive — enforced by the Zod refine on writes, the route layer, and a DB CHECK
constraint (`medication_schedules_rrule_xor_rolling`). v1.7.0 adds `scheduleType` (SCHEDULED / PRN / CYCLIC) and
the cyclic on/off-week fields.
CycleDayLogReadEnvelope:
type: object
properties:
data:
anyOf:
- $ref: "#/components/schemas/CycleDayLogDTO"
- type: "null"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
CycleDayLogDTO:
type: object
properties:
id:
type: string
date:
type: string
cycleId:
anyOf:
- type: string
- type: "null"
flow:
anyOf:
- type: string
enum:
- NONE
- SPOTTING
- LIGHT
- MEDIUM
- HEAVY
- type: "null"
intermenstrualBleeding:
type: boolean
basalBodyTempC:
anyOf:
- type: number
- type: "null"
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"
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
- ovulationTest
- cervicalMucus
- 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"
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"
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"
required:
- date
- phase
- isPredictedPeriod
- isFertileWindow
- isPredictedOvulation
- isPeriodLogged
- flow
- hasSymptoms
- confidence
- basalBodyTempC
- ovulationTest
- cervicalMucus
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
- days
- meta
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
CyclePredictionDTO:
type: object
properties:
method:
$ref: "#/components/schemas/CyclePredictionMethod"
nextPeriodStart:
type: string
nextPeriodStartLow:
type: string
nextPeriodStartHigh:
type: string
fertileWindowStart:
anyOf:
- type: string
- type: "null"
fertileWindowEnd:
anyOf:
- type: string
- type: "null"
predictedOvulation:
anyOf:
- type: string
- type: "null"
ovulationConfirmed:
type: boolean
confidence:
type: number
cyclesObserved:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
stillLearning:
type: boolean
disclaimer:
type: string
required:
- method
- nextPeriodStart
- nextPeriodStartLow
- nextPeriodStartHigh
- fertileWindowStart
- fertileWindowEnd
- predictedOvulation
- ovulationConfirmed
- confidence
- cyclesObserved
- stillLearning
- disclaimer
additionalProperties: false
description: The materialised forecast. Fertile-window fields (and predictedOvulation/ovulationConfirmed) are
server-suppressed (null/false) unless the goal is TRYING_TO_CONCEIVE or AVOID_PREGNANCY.
CyclePredictionMethod:
type: string
enum:
- CALENDAR
- SYMPTOTHERMAL
- TEMPERATURE_TREND
- BLENDED
CyclePhase:
type: string
enum:
- MENSTRUAL
- FOLLICULAR
- OVULATORY
- LUTEAL
CycleHistoryEnvelope:
type: object
properties:
data:
type: object
properties:
cycles:
type: array
items:
$ref: "#/components/schemas/MenstrualCycleDTO"
stats:
type: object
properties:
avgLengthDays:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
lengthVariabilityDays:
anyOf:
- type: number
- type: "null"
avgPeriodLengthDays:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
regularity:
type: string
enum:
- REGULAR
- IRREGULAR
- LEARNING
required:
- avgLengthDays
- lengthVariabilityDays
- avgPeriodLengthDays
- regularity
additionalProperties: false
required:
- cycles
- stats
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
CycleInsightsEnvelope:
type: object
properties:
data:
type: object
properties:
rows:
type: array
items:
$ref: "#/components/schemas/CyclePhaseCrosstabRow"
headline:
anyOf:
- $ref: "#/components/schemas/CyclePhaseCrosstabRow"
- type: "null"
lagged:
type: object
properties:
discovered:
type: array
items:
$ref: "#/components/schemas/CyclePhaseLaggedPair"
pairsTested:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
fdrQ:
type: number
minPairs:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
required:
- discovered
- pairsTested
- fdrQ
- minPairs
additionalProperties: false
symptomPatterns:
type: array
items:
$ref: "#/components/schemas/CycleSymptomPhaseRow"
contrast:
type: object
properties:
high:
type: string
const: LUTEAL
low:
type: string
const: FOLLICULAR
required:
- high
- low
additionalProperties: false
windowDays:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
cyclesObserved:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
required:
- rows
- headline
- lagged
- symptomPatterns
- contrast
- windowDays
- cyclesObserved
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
CyclePhaseCrosstabRow:
type: object
properties:
metricKey:
type: string
enum:
- restingHeartRate
- heartRateVariability
- sleepDuration
- steps
- weight
- basalBodyTemp
- wristTemperature
- skinTemperature
- bloodGlucose
- mood
display:
type: string
enum:
- hours
- steps
- bpm
- ms
- kg
- celsius
- glucose
- mood
lutealDays:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
follicularDays:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
lutealAvg:
type: number
follicularAvg:
type: number
delta:
type: number
pValue:
type: number
qValue:
type: number
confidence:
type: string
enum:
- low
- medium
- high
required:
- metricKey
- display
- lutealDays
- follicularDays
- lutealAvg
- follicularAvg
- delta
- pValue
- qValue
- confidence
additionalProperties: false
description: One FDR-surviving luteal-vs-follicular contrast for an outcome metric. `delta = lutealAvg − follicularAvg`.
Observational, never causal.
CyclePhaseLaggedPair:
type: object
properties:
behaviour:
type: string
outcome:
type: string
n:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
r:
type: number
pValue:
type: number
qValue:
type: number
interpretation:
type: string
lagDays:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
required:
- behaviour
- outcome
- n
- r
- pValue
- qValue
- interpretation
- lagDays
additionalProperties: false
description: One FDR-surviving lagged-Pearson pair from the continuous CYCLE_PHASE ordinal × outcome matrix (mechanism
B). Descriptive, never causal.
CycleSymptomPhaseRow:
type: object
properties:
symptomKey:
type: string
counts:
type: object
properties:
MENSTRUAL:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
FOLLICULAR:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
OVULATORY:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
LUTEAL:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
required:
- MENSTRUAL
- FOLLICULAR
- OVULATORY
- LUTEAL
additionalProperties: false
total:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
topPhase:
type: string
enum:
- MENSTRUAL
- FOLLICULAR
- OVULATORY
- LUTEAL
topShare:
type: number
required:
- symptomKey
- counts
- total
- topPhase
- topShare
additionalProperties: false
description: Where a logged symptom clusters across the cycle phases. Surfaced only once logged on ≥3 phase-labelled
days. Observational, never causal.
CyclePurgeEnvelope:
type: object
properties:
data:
type: object
properties:
purged:
type: boolean
dayLogs:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
predictions:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
cycles:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
auditRows:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
pushRows:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
required:
- purged
- dayLogs
- predictions
- cycles
- auditRows
- pushRows
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
CycleProfileEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/CycleProfileDTO"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
CycleProfileDTO:
type: object
properties:
goal:
type: string
enum:
- GENERAL_HEALTH
- AVOID_PREGNANCY
- TRYING_TO_CONCEIVE
- PERIMENOPAUSE
- OFF
cycleTrackingEnabled:
type: boolean
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
- rawChartMode
- predictionEnabled
- discreetNotifications
- sensitiveCategoryEncryption
- typicalCycleLength
- typicalPeriodLength
- lutealPhaseLength
- updatedAt
additionalProperties: false
description: The full per-user cycle settings row.
CyclePrefsGetEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/CycleProfileDTO"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
CyclePrefsPatchEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/CycleProfileDTO"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
CapabilitiesEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/CapabilitiesResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
CapabilitiesResponse:
type: object
properties:
apiContractVersion:
type: string
description: Running build version — mirrors GET /api/version `version`.
derivedMetricIds:
type: array
items:
type: string
description: Closed derived-metric id set (GET /api/insights/derived).
vitalsBaselineTypes:
type: array
items:
type: string
description: MeasurementTypes the typical-range baseline engine supports.
layoutTileIds:
type: array
items:
type: string
description: Canonical insights layout tile-id set (English ids).
metricStatusIds:
type: array
items:
type: string
description: Closed metric-status / assessment id set.
ingest:
type: object
properties:
quantityTypes:
type: array
items:
type: object
properties:
type:
type: string
description: HealthLog MeasurementType.
hk:
type: string
description: HealthKit identifier.
unit:
type: string
description: Canonical DB unit.
required:
- type
- hk
- unit
additionalProperties: false
description: Accepted HealthKit quantity-sample mappings.
eventTypes:
type: array
items:
type: string
description: MeasurementTypes for device-flagged EVENT-class HealthKit samples.
computedScores:
type: array
items:
type: string
description: Server-owned nightly composite score types.
writeAllowlist:
type: array
items:
type: string
description: MeasurementSources a client may attribute on a write (others are server-owned).
required:
- quantityTypes
- eventTypes
- computedScores
- writeAllowlist
additionalProperties: false
description: Ingest vocabularies the batch / single-write paths accept.
fhir:
type: object
properties:
atcSystem:
type: string
description: WHO ATC CodeSystem URI.
snomedRoute:
type: string
description: SNOMED CT CodeSystem URI.
germanAtcDefaultLocales:
type: array
items:
type: string
description: App locales that default the additive BfArM ATC coding on.
restBaseUrl:
type: string
description: Base path of the read-only FHIR R4 REST face.
readScope:
type: string
description: Bearer scope a narrow token needs to read the FHIR face.
resourceTypes:
type: array
items:
type: string
description: FHIR resource types the REST face serves (read + search).
operations:
type: array
items:
type: string
description: Whole-record operations exposed (e.g. $everything).
searchParams:
type: array
items:
type: string
description: Search parameters honoured uniformly across the search routes.
required:
- atcSystem
- snomedRoute
- germanAtcDefaultLocales
- restBaseUrl
- readScope
- resourceTypes
- operations
- searchParams
additionalProperties: false
description: FHIR coding constants + the read-only REST face descriptor (v1.11).
share:
type: object
properties:
supported:
type: boolean
description: Whether clinician share links are served.
maxDays:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
description: Maximum lifetime of a share link, in days. No never-expiring share.
resourceTypes:
type: array
items:
type: string
description: FHIR resource types a share link may be scoped to serve.
sections:
type: array
items:
type: string
description: Scopeable report sections a share link may toggle.
required:
- supported
- maxDays
- resourceTypes
- sections
additionalProperties: false
description: Clinician share-link surface descriptor (v1.11).
required:
- apiContractVersion
- derivedMetricIds
- vitalsBaselineTypes
- layoutTileIds
- metricStatusIds
- ingest
- fhir
- share
additionalProperties: false
description: Live id vocabularies + contract version. Every list is derived server-side from the canonical registry it
documents, so it cannot drift from the values the routes actually accept/emit.
ShareLinkCreated:
type: object
properties:
data:
type: object
properties:
id:
type: string
label:
type: string
rangeStart:
type: string
rangeEnd:
anyOf:
- type: string
- type: "null"
resourceTypes:
type: array
items:
type: string
allowFhirApi:
type: boolean
expiresAt:
type: string
createdAt:
type: string
revokedAt:
anyOf:
- type: string
- type: "null"
lastAccessAt:
anyOf:
- type: string
- type: "null"
accessCount:
type: number
active:
type: boolean
token:
type: string
description: Raw `hls_` token — returned ONCE and unrecoverable thereafter.
required:
- id
- label
- rangeStart
- rangeEnd
- resourceTypes
- allowFhirApi
- expiresAt
- createdAt
- revokedAt
- lastAccessAt
- accessCount
- active
- token
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
ShareLinkList:
type: object
properties:
data:
type: object
properties:
shareLinks:
type: array
items:
type: object
properties:
id:
type: string
label:
type: string
rangeStart:
type: string
rangeEnd:
anyOf:
- type: string
- type: "null"
resourceTypes:
type: array
items:
type: string
allowFhirApi:
type: boolean
expiresAt:
type: string
createdAt:
type: string
revokedAt:
anyOf:
- type: string
- type: "null"
lastAccessAt:
anyOf:
- type: string
- type: "null"
accessCount:
type: number
active:
type: boolean
required:
- id
- label
- rangeStart
- rangeEnd
- resourceTypes
- allowFhirApi
- expiresAt
- createdAt
- revokedAt
- lastAccessAt
- accessCount
- active
additionalProperties: false
required:
- shareLinks
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
ShareLinkRevoked:
type: object
properties:
data:
type: object
properties:
id:
type: string
revoked:
type: boolean
required:
- id
- revoked
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
LoginResponse:
type: object
properties:
data:
$ref: "#/components/schemas/AccessRefreshBundle"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
AccessRefreshBundle:
type: object
properties:
user:
type: object
properties:
id:
type: string
username:
type: string
required:
- id
- username
additionalProperties: false
token:
type: string
description: Access token (`hlk_<64hex>`).
tokenExpiresAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
refreshToken:
description: Refresh token (`hlr_<64hex>`); only present for native-policy callers.
type: string
refreshTokenExpiresAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
required:
- token
- tokenExpiresAt
additionalProperties: false
description: Native-client token bundle returned by login + refresh. Web cookie-only callers see only `user`.
PasskeyLoginVerifyResponse:
type: object
properties:
data:
$ref: "#/components/schemas/AccessRefreshBundle"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
RefreshResponse:
type: object
properties:
data:
$ref: "#/components/schemas/AccessRefreshBundle"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
SyncStateEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/SyncStateResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
SyncStateResponse:
type: object
properties:
userId:
type: string
timezone:
type: string
lastSyncedAt:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
serverNow:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
measurements:
type: object
properties:
lastUpdatedAt:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
liveCount:
type: integer
minimum: 0
maximum: 9007199254740991
tombstonedCount:
type: integer
minimum: 0
maximum: 9007199254740991
required:
- lastUpdatedAt
- liveCount
- tombstonedCount
additionalProperties: false
mood:
type: object
properties:
lastUpdatedAt:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
liveCount:
type: integer
minimum: 0
maximum: 9007199254740991
tombstonedCount:
type: integer
minimum: 0
maximum: 9007199254740991
required:
- lastUpdatedAt
- liveCount
- tombstonedCount
additionalProperties: false
intakes:
type: object
properties:
lastUpdatedAt:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
liveCount:
type: integer
minimum: 0
maximum: 9007199254740991
tombstonedCount:
type: integer
minimum: 0
maximum: 9007199254740991
required:
- lastUpdatedAt
- liveCount
- tombstonedCount
additionalProperties: false
sync:
type: object
properties:
incrementalWindowDays:
type: integer
exclusiveMinimum: 0
maximum: 9007199254740991
description: Days an incremental delta stays valid; tracks the native refresh-token lifetime. Beyond it a device
re-pairs with a full backfill. iOS derives its window from this rather than hardcoding 60.
tombstoneRetentionDays:
type: integer
exclusiveMinimum: 0
maximum: 9007199254740991
description: Horizon past which tombstones may be pruned. A cursor older than this gets `cursorExpired` on
`/api/sync/changes`.
required:
- incrementalWindowDays
- tombstoneRetentionDays
additionalProperties: false
description: Sync-window metadata the client reads instead of hardcoding.
required:
- userId
- timezone
- lastSyncedAt
- serverNow
- measurements
- mood
- intakes
- sync
additionalProperties: false
description: iOS SyncMode handshake. Each GET also advances the server-side `lastSyncedAt` checkpoint and returns the
previous value. The cheap 'should I sync?' summary; the durable delta cursor lives on `/api/sync/changes`.
SyncChangesEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/SyncChangesResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
SyncChangesResponse:
type: object
properties:
serverNow:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
cursor:
anyOf:
- type: string
- type: "null"
description: Opaque cursor to echo into the next request.
hasMore:
type: boolean
description: False once the client is caught up as of `serverNow`.
cursorExpired:
type: boolean
description: True when the supplied cursor predates tombstone retention — drop the cursor and do a clean initial sync.
changes:
type: object
properties:
measurements:
type: object
properties:
upserts:
type: array
items:
$ref: "#/components/schemas/SyncMeasurementUpsert"
tombstones:
type: array
items:
$ref: "#/components/schemas/SyncMeasurementTombstone"
required:
- upserts
- tombstones
additionalProperties: false
mood:
type: object
properties:
upserts:
type: array
items:
$ref: "#/components/schemas/SyncMoodUpsert"
tombstones:
type: array
items:
$ref: "#/components/schemas/SyncMoodTombstone"
required:
- upserts
- tombstones
additionalProperties: false
intakes:
type: object
properties:
upserts:
type: array
items:
$ref: "#/components/schemas/SyncIntakeUpsert"
tombstones:
type: array
items:
$ref: "#/components/schemas/SyncIntakeTombstone"
required:
- upserts
- tombstones
additionalProperties: false
required:
- measurements
- mood
- intakes
additionalProperties: false
required:
- serverNow
- cursor
- hasMore
- cursorExpired
- changes
additionalProperties: false
description: "Multi-domain delta page (v1.7.0): measurements + mood + intakes. One opaque multi-domain keyset cursor;
tombstones apply before upserts within each domain. Tombstone identity: measurements key on externalId, mood +
intakes on server id. The iOS consumer is measurements-only this cycle; mood + intakes are forward-prep."
SyncMeasurementUpsert:
type: object
properties:
id:
type: string
type:
type: string
enum:
- WEIGHT
- BLOOD_PRESSURE_SYS
- BLOOD_PRESSURE_DIA
- PULSE
- BODY_FAT
- SLEEP_DURATION
- ACTIVITY_STEPS
- BLOOD_GLUCOSE
- TOTAL_BODY_WATER
- BONE_MASS
- OXYGEN_SATURATION
- HEART_RATE_VARIABILITY
- RESTING_HEART_RATE
- ACTIVE_ENERGY_BURNED
- FLIGHTS_CLIMBED
- WALKING_RUNNING_DISTANCE
- VO2_MAX
- BODY_TEMPERATURE
- FAT_FREE_MASS
- FAT_MASS
- MUSCLE_MASS
- SKIN_TEMPERATURE
- PULSE_WAVE_VELOCITY
- VASCULAR_AGE
- VISCERAL_FAT
- AUDIO_EXPOSURE_ENV
- AUDIO_EXPOSURE_HEADPHONE
- TIME_IN_DAYLIGHT
- WALKING_STEADINESS
- AUDIO_EXPOSURE_EVENT
- RESPIRATORY_RATE
- BODY_MASS_INDEX
- LEAN_BODY_MASS
- WALKING_HEART_RATE_AVERAGE
- WALKING_ASYMMETRY
- WALKING_DOUBLE_SUPPORT
- WALKING_STEP_LENGTH
- WALKING_SPEED
- CARDIO_RECOVERY
- WRIST_TEMPERATURE
- FALL_COUNT
- SIX_MINUTE_WALK_DISTANCE
- STAIR_ASCENT_SPEED
- STAIR_DESCENT_SPEED
- BREATHING_DISTURBANCES
- IRREGULAR_RHYTHM_NOTIFICATION
- HIGH_HEART_RATE_EVENT
- LOW_HEART_RATE_EVENT
- WALKING_STEADINESS_EVENT
- BREATHING_DISTURBANCE_EVENT
- RECOVERY_SCORE
- STRESS_SCORE
- STRAIN_SCORE
- HRV_RMSSD
- DAY_STRAIN
- WORKOUT_STRAIN
- SLEEP_PERFORMANCE
- SLEEP_EFFICIENCY
- SLEEP_CONSISTENCY
- SLEEP_NEED
- ENERGY_EXPENDITURE_KJ
- AVERAGE_HEART_RATE
- MAX_HEART_RATE
- SLEEP_DISTURBANCE_COUNT
value:
type: number
unit:
type: string
measuredAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
source:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
notes:
anyOf:
- type: string
- type: "null"
externalId:
anyOf:
- type: string
- type: "null"
description: Cross-device dedup key (UUID string or `stats:<id>:<date>`).
syncVersion:
type: integer
exclusiveMinimum: 0
maximum: 9007199254740991
description: LWW reconciliation counter; echo to keep the mirror monotonic.
updatedAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
required:
- id
- type
- value
- unit
- measuredAt
- source
- externalId
- syncVersion
- updatedAt
additionalProperties: false
SyncMeasurementTombstone:
type: object
properties:
id:
type: string
externalId:
anyOf:
- type: string
- type: "null"
description: The identity key the client dedups on for measurements.
syncVersion:
type: integer
exclusiveMinimum: 0
maximum: 9007199254740991
deletedAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
updatedAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
required:
- id
- externalId
- syncVersion
- deletedAt
- updatedAt
additionalProperties: false
description: A soft-deleted measurement. Apply tombstones BEFORE upserts within a page to avoid resurrecting a row.
SyncMoodUpsert:
type: object
properties:
id:
type: string
date:
type: string
description: YYYY-MM-DD anchored to the row's `tz`.
mood:
type: string
score:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
tags:
anyOf:
- type: string
- type: "null"
description: JSON array of tag keys, or null.
note:
anyOf:
- type: string
- type: "null"
moodLoggedAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
source:
type: string
syncVersion:
type: integer
minimum: 0
maximum: 9007199254740991
description: LWW reconciliation counter; mood is last-writer-wins by it.
updatedAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
required:
- id
- date
- mood
- score
- tags
- note
- moodLoggedAt
- source
- syncVersion
- updatedAt
additionalProperties: false
SyncMoodTombstone:
type: object
properties:
id:
type: string
description: Server id — the identity key the client dedups on for mood.
syncVersion:
type: integer
minimum: 0
maximum: 9007199254740991
deletedAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
updatedAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
required:
- id
- syncVersion
- deletedAt
- updatedAt
additionalProperties: false
description: A soft-deleted mood entry, keyed on server `id`. Apply before upserts within the domain page.
SyncIntakeUpsert:
type: object
properties:
id:
type: string
medicationId:
type: string
scheduledFor:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
takenAt:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
skipped:
type: boolean
source:
type: string
syncVersion:
type: integer
minimum: 0
maximum: 9007199254740991
description: Reconciliation counter. An intake is immutable; a correction is a tombstone + re-insert.
updatedAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
required:
- id
- medicationId
- scheduledFor
- takenAt
- skipped
- source
- syncVersion
- updatedAt
additionalProperties: false
SyncIntakeTombstone:
type: object
properties:
id:
type: string
description: Server id — the identity key the client dedups on for intakes.
syncVersion:
type: integer
minimum: 0
maximum: 9007199254740991
deletedAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
updatedAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
required:
- id
- syncVersion
- deletedAt
- updatedAt
additionalProperties: false
description: A soft-deleted medication intake, keyed on server `id`. Apply before upserts within the domain page.
ListMeasurementsResponse:
type: object
properties:
data:
type: object
properties:
measurements:
type: array
items:
$ref: "#/components/schemas/MeasurementResource"
total:
type: integer
minimum: 0
maximum: 9007199254740991
required:
- measurements
- total
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
MeasurementResource:
type: object
properties:
id:
type: string
type:
type: string
enum:
- WEIGHT
- BLOOD_PRESSURE_SYS
- BLOOD_PRESSURE_DIA
- PULSE
- BODY_FAT
- SLEEP_DURATION
- ACTIVITY_STEPS
- BLOOD_GLUCOSE
- TOTAL_BODY_WATER
- BONE_MASS
- OXYGEN_SATURATION
- HEART_RATE_VARIABILITY
- RESTING_HEART_RATE
- ACTIVE_ENERGY_BURNED
- FLIGHTS_CLIMBED
- WALKING_RUNNING_DISTANCE
- VO2_MAX
- BODY_TEMPERATURE
- FAT_FREE_MASS
- FAT_MASS
- MUSCLE_MASS
- SKIN_TEMPERATURE
- PULSE_WAVE_VELOCITY
- VASCULAR_AGE
- VISCERAL_FAT
- AUDIO_EXPOSURE_ENV
- AUDIO_EXPOSURE_HEADPHONE
- TIME_IN_DAYLIGHT
- WALKING_STEADINESS
- AUDIO_EXPOSURE_EVENT
- RESPIRATORY_RATE
- BODY_MASS_INDEX
- LEAN_BODY_MASS
- WALKING_HEART_RATE_AVERAGE
- WALKING_ASYMMETRY
- WALKING_DOUBLE_SUPPORT
- WALKING_STEP_LENGTH
- WALKING_SPEED
- CARDIO_RECOVERY
- WRIST_TEMPERATURE
- FALL_COUNT
- SIX_MINUTE_WALK_DISTANCE
- STAIR_ASCENT_SPEED
- STAIR_DESCENT_SPEED
- BREATHING_DISTURBANCES
- IRREGULAR_RHYTHM_NOTIFICATION
- HIGH_HEART_RATE_EVENT
- LOW_HEART_RATE_EVENT
- WALKING_STEADINESS_EVENT
- BREATHING_DISTURBANCE_EVENT
- RECOVERY_SCORE
- STRESS_SCORE
- STRAIN_SCORE
- HRV_RMSSD
- DAY_STRAIN
- WORKOUT_STRAIN
- SLEEP_PERFORMANCE
- SLEEP_EFFICIENCY
- SLEEP_CONSISTENCY
- SLEEP_NEED
- ENERGY_EXPENDITURE_KJ
- AVERAGE_HEART_RATE
- MAX_HEART_RATE
- SLEEP_DISTURBANCE_COUNT
value:
type: number
unit:
type: string
measuredAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
source:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
notes:
anyOf:
- type: string
- type: "null"
required:
- id
- type
- value
- unit
- measuredAt
- source
additionalProperties: false
description: Server-shaped measurement row returned by GET endpoints.
CreateMeasurementResponse:
type: object
properties:
data:
$ref: "#/components/schemas/MeasurementResource"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
BatchMeasurementsResponse:
type: object
properties:
data:
$ref: "#/components/schemas/AppleHealthBatchResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
AppleHealthBatchResponse:
type: object
properties:
inserted:
type: integer
minimum: 0
maximum: 9007199254740991
duplicate:
type: integer
minimum: 0
maximum: 9007199254740991
skipped:
type: integer
minimum: 0
maximum: 9007199254740991
entries:
type: array
items:
$ref: "#/components/schemas/AppleHealthBatchEntryResult"
required:
- inserted
- duplicate
- skipped
- entries
additionalProperties: false
AppleHealthBatchEntryResult:
type: object
properties:
index:
type: integer
minimum: 0
maximum: 9007199254740991
status:
type: string
enum:
- inserted
- duplicate
- skipped
reason:
type: string
required:
- index
- status
additionalProperties: false
MeasurementsDeleteByExternalIdsResponse:
type: object
properties:
data:
$ref: "#/components/schemas/MeasurementsDeleteByExternalIdsResult"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
MeasurementsDeleteByExternalIdsResult:
type: object
properties:
deletedCount:
type: integer
minimum: 0
maximum: 9007199254740991
required:
- deletedCount
additionalProperties: false
SleepNightResponse:
type: object
properties:
data:
$ref: "#/components/schemas/SleepNightResource"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
SleepNightResource:
type: object
properties:
night:
anyOf:
- type: string
- type: "null"
main:
anyOf:
- type: object
properties:
night:
type: string
description: Wake-day key (YYYY-MM-DD).
source:
anyOf:
- type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
- type: "null"
start:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
end:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
asleepMinutes:
type: integer
minimum: 0
maximum: 9007199254740991
inBedMinutes:
anyOf:
- type: integer
minimum: 0
maximum: 9007199254740991
- type: "null"
awakeMinutes:
anyOf:
- type: integer
minimum: 0
maximum: 9007199254740991
- type: "null"
awakenings:
type: integer
minimum: 0
maximum: 9007199254740991
stages:
type: object
propertyNames:
type: string
enum:
- IN_BED
- AWAKE
- ASLEEP
- REM
- CORE
- DEEP
additionalProperties:
type: number
required:
- IN_BED
- AWAKE
- ASLEEP
- REM
- CORE
- DEEP
segments:
type: array
items:
type: object
properties:
stage:
anyOf:
- type: string
enum:
- IN_BED
- AWAKE
- ASLEEP
- REM
- CORE
- DEEP
- type: "null"
start:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
end:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
minutes:
type: integer
minimum: 0
maximum: 9007199254740991
required:
- stage
- start
- end
- minutes
additionalProperties: false
required:
- night
- source
- start
- end
- asleepMinutes
- inBedMinutes
- awakeMinutes
- awakenings
- stages
- segments
additionalProperties: false
- type: "null"
naps:
type: array
items:
type: object
properties:
night:
type: string
description: Wake-day key (YYYY-MM-DD).
source:
anyOf:
- type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
- type: "null"
start:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
end:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
asleepMinutes:
type: integer
minimum: 0
maximum: 9007199254740991
inBedMinutes:
anyOf:
- type: integer
minimum: 0
maximum: 9007199254740991
- type: "null"
awakeMinutes:
anyOf:
- type: integer
minimum: 0
maximum: 9007199254740991
- type: "null"
awakenings:
type: integer
minimum: 0
maximum: 9007199254740991
stages:
type: object
propertyNames:
type: string
enum:
- IN_BED
- AWAKE
- ASLEEP
- REM
- CORE
- DEEP
additionalProperties:
type: number
required:
- IN_BED
- AWAKE
- ASLEEP
- REM
- CORE
- DEEP
segments:
type: array
items:
type: object
properties:
stage:
anyOf:
- type: string
enum:
- IN_BED
- AWAKE
- ASLEEP
- REM
- CORE
- DEEP
- type: "null"
start:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
end:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
minutes:
type: integer
minimum: 0
maximum: 9007199254740991
required:
- stage
- start
- end
- minutes
additionalProperties: false
required:
- night
- source
- start
- end
- asleepMinutes
- inBedMinutes
- awakeMinutes
- awakenings
- stages
- segments
additionalProperties: false
required:
- night
- main
- naps
additionalProperties: false
description: "One reconstructed sleep night: the main session's hypnogram segments + breakdown, plus same-wake-day naps
surfaced separately."
ListWorkoutsResponse:
type: object
properties:
data:
$ref: "#/components/schemas/WorkoutListResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
WorkoutListResponse:
type: object
properties:
workouts:
type: array
items:
$ref: "#/components/schemas/WorkoutListEntry"
meta:
type: object
properties:
total:
type: integer
minimum: 0
maximum: 9007199254740991
limit:
type: integer
exclusiveMinimum: 0
maximum: 9007199254740991
offset:
type: integer
minimum: 0
maximum: 9007199254740991
droppedDuplicates:
type: integer
minimum: 0
maximum: 9007199254740991
required:
- total
- limit
- offset
- droppedDuplicates
additionalProperties: false
required:
- workouts
- meta
additionalProperties: false
WorkoutListEntry:
type: object
properties:
id:
type: string
sportType:
type: string
startedAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
endedAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
durationSec:
type: integer
minimum: 0
maximum: 9007199254740991
distanceM:
anyOf:
- type: number
- type: "null"
activeEnergyKcal:
anyOf:
- type: number
- type: "null"
avgHr:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
maxHr:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
source:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
externalId:
anyOf:
- type: string
- type: "null"
required:
- id
- sportType
- startedAt
- endedAt
- durationSec
- distanceM
- activeEnergyKcal
- avgHr
- maxHr
- source
- externalId
additionalProperties: false
GetWorkoutResponse:
type: object
properties:
data:
$ref: "#/components/schemas/WorkoutDetailResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
WorkoutDetailResponse:
type: object
properties:
id:
type: string
sportType:
type: string
startedAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
endedAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
durationSec:
type: integer
minimum: 0
maximum: 9007199254740991
distanceM:
anyOf:
- type: number
- type: "null"
activeEnergyKcal:
anyOf:
- type: number
- type: "null"
avgHr:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
maxHr:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
minHr:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
stepCount:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
elevationM:
anyOf:
- type: number
- type: "null"
pauseDurationSec:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
source:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
externalId:
anyOf:
- type: string
- type: "null"
metadata:
anyOf:
- {}
- type: "null"
route:
anyOf:
- $ref: "#/components/schemas/WorkoutRouteGeometry"
- type: "null"
samples:
anyOf:
- $ref: "#/components/schemas/WorkoutHrSeries"
- type: "null"
canonicalId:
type: string
required:
- id
- sportType
- startedAt
- endedAt
- durationSec
- distanceM
- activeEnergyKcal
- avgHr
- maxHr
- minHr
- stepCount
- elevationM
- pauseDurationSec
- source
- externalId
- metadata
- route
- samples
- canonicalId
additionalProperties: false
WorkoutRouteGeometry:
type: object
properties:
geometry: {}
sampleTimestamps:
anyOf:
- type: array
items:
type: string
- type: "null"
required:
- geometry
- sampleTimestamps
additionalProperties: false
WorkoutHrSeries:
type: object
properties:
sampleCount:
type: integer
minimum: 0
maximum: 9007199254740991
samples: {}
required:
- sampleCount
- samples
additionalProperties: false
BatchWorkoutsResponse:
type: object
properties:
data:
$ref: "#/components/schemas/WorkoutBatchResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
WorkoutBatchResponse:
type: object
properties:
processed:
type: integer
minimum: 0
maximum: 9007199254740991
inserted:
type: integer
minimum: 0
maximum: 9007199254740991
duplicates:
type: integer
minimum: 0
maximum: 9007199254740991
skipped:
type: array
items:
type: object
properties:
index:
type: integer
minimum: 0
maximum: 9007199254740991
reason:
type: string
required:
- index
- reason
additionalProperties: false
entries:
type: array
items:
$ref: "#/components/schemas/WorkoutBatchEntryResult"
required:
- processed
- inserted
- duplicates
- skipped
- entries
additionalProperties: false
WorkoutBatchEntryResult:
type: object
properties:
index:
type: integer
minimum: 0
maximum: 9007199254740991
status:
type: string
enum:
- inserted
- duplicate
- skipped
reason:
type: string
required:
- index
- status
additionalProperties: false
DeviceRegisterResponse:
type: object
properties:
data:
type: object
properties:
id:
type: string
required:
- id
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
ListMedicationsResponse:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/MedicationListEntry"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
MedicationListEntry:
type: object
properties:
id:
type: string
name:
type: string
dose:
type: string
treatmentClass:
$ref: "#/components/schemas/MedicationTreatmentClass"
dosesPerUnit:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
description: Doses per pen / vial for inventory tracking. NULL = inventory tracking off.
active:
type: boolean
notificationsEnabled:
type: boolean
liveActivityEnabled:
type: boolean
description: v1.7.0 iOS Live Activity opt-in for this medication's reminders. Default false. The iOS client owns the
ActivityKit lifecycle; the server only stores + echoes the flag.
criticalAlarmEnabled:
type: boolean
description: v1.7.0 iOS 26 AlarmKit critical-reminder opt-in. Default false. Critical alarms bypass the device mute
switch / Focus; the server stores the preference only.
atcCode:
anyOf:
- type: string
- type: "null"
description: v1.9.0 optional WHO ATC classification code (active-substance class, e.g. `A10BX10`).
User/clinician-asserted; never machine-guessed. Emitted on the FHIR `medicationCodeableConcept` under
`http://www.whocc.no/atc`. NULL = no code captured.
rxNormCode:
anyOf:
- type: string
- type: "null"
description: v1.9.0 optional RxNorm RxCUI (numeric, US identifier, e.g. `2601723`). Secondary FHIR coding under
`http://www.nlm.nih.gov/research/umls/rxnorm`, alongside any ATC code. NULL = no code captured.
pausedAt:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
snoozedUntil:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
nextDueAt:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
description: v1.7.0 server-computed next due instant across all the medication's schedules (earliest
`nextOccurrenceAfter`). Read-only — computed, not stored. NULL when no schedule has an upcoming slot
(paused, one-shot in the past, `endsOn` crossed, every schedule PRN). The list GET is cached 60 s, so a 60 s
staleness is accepted.
startsOn:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
description: v1.5 course start (ISO date). Anchors RRULE BYDAY / BYMONTHDAY patterns and the rolling-interval
countdown's first window. NULL means active from creation.
endsOn:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
description: v1.5 course end (ISO date). NULL means chronic. Equals `startsOn` when `oneShot` is true.
oneShot:
type: boolean
description: v1.5 single-administration flag. When true the medication has at most one schedule (no `rrule` /
`rollingIntervalDays`), and `active` auto-flips to false once the dose is logged.
createdAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
updatedAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
schedules:
type: array
items:
$ref: "#/components/schemas/MedicationScheduleOutput"
category:
$ref: "#/components/schemas/MedicationCategory"
lastTakenAt:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
description: Latest non-skipped `MedicationIntakeEvent.takenAt` for the medication. Drives the rolling-cadence countdown
surface.
todayEventCount:
type: integer
minimum: 0
maximum: 9007199254740991
description: Number of intake events scheduled for today (user-local day window).
required:
- id
- name
- dose
- treatmentClass
- dosesPerUnit
- active
- notificationsEnabled
- liveActivityEnabled
- criticalAlarmEnabled
- atcCode
- rxNormCode
- pausedAt
- snoozedUntil
- nextDueAt
- startsOn
- endsOn
- oneShot
- createdAt
- updatedAt
- schedules
- category
- lastTakenAt
- todayEventCount
additionalProperties: false
description: List-row variant of the medication resource enriched with the joined `category`, `lastTakenAt`, and
`todayEventCount` fields the dashboard + iOS client consume. The base medication fields (`id`, `name`, `dose`,
`treatmentClass`, `dosesPerUnit`, `active`, `notificationsEnabled`, `pausedAt`, `snoozedUntil`, `startsOn`,
`endsOn`, `oneShot`, `createdAt`, `updatedAt`, `schedules`) are inlined; see the `Medication` component for
their semantics.
MedicationCategory:
type: string
enum:
- BLOOD_PRESSURE
- VITAMIN
- SUPPLEMENT
- PAIN_RELIEF
- ALLERGY
- DIGESTIVE
- THYROID
- HORMONE
- SKIN
- SLEEP_AID
- DIABETES
- ANTIBIOTIC
- OTHER
description: Clinical taxonomy stored in the `medication_categories` side-table. Orthogonal to `MedicationTreatmentClass`.
CreateMedicationResponse:
type: object
properties:
data:
$ref: "#/components/schemas/MedicationDetail"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
MedicationDetail:
type: object
properties:
id:
type: string
name:
type: string
dose:
type: string
treatmentClass:
$ref: "#/components/schemas/MedicationTreatmentClass"
dosesPerUnit:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
description: Doses per pen / vial for inventory tracking. NULL = inventory tracking off.
active:
type: boolean
notificationsEnabled:
type: boolean
liveActivityEnabled:
type: boolean
description: v1.7.0 iOS Live Activity opt-in for this medication's reminders. Default false. The iOS client owns the
ActivityKit lifecycle; the server only stores + echoes the flag.
criticalAlarmEnabled:
type: boolean
description: v1.7.0 iOS 26 AlarmKit critical-reminder opt-in. Default false. Critical alarms bypass the device mute
switch / Focus; the server stores the preference only.
atcCode:
anyOf:
- type: string
- type: "null"
description: v1.9.0 optional WHO ATC classification code (active-substance class, e.g. `A10BX10`).
User/clinician-asserted; never machine-guessed. Emitted on the FHIR `medicationCodeableConcept` under
`http://www.whocc.no/atc`. NULL = no code captured.
rxNormCode:
anyOf:
- type: string
- type: "null"
description: v1.9.0 optional RxNorm RxCUI (numeric, US identifier, e.g. `2601723`). Secondary FHIR coding under
`http://www.nlm.nih.gov/research/umls/rxnorm`, alongside any ATC code. NULL = no code captured.
pausedAt:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
snoozedUntil:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
nextDueAt:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
description: v1.7.0 server-computed next due instant across all the medication's schedules (earliest
`nextOccurrenceAfter`). Read-only — computed, not stored. NULL when no schedule has an upcoming slot
(paused, one-shot in the past, `endsOn` crossed, every schedule PRN). The list GET is cached 60 s, so a 60 s
staleness is accepted.
startsOn:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
description: v1.5 course start (ISO date). Anchors RRULE BYDAY / BYMONTHDAY patterns and the rolling-interval
countdown's first window. NULL means active from creation.
endsOn:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
description: v1.5 course end (ISO date). NULL means chronic. Equals `startsOn` when `oneShot` is true.
oneShot:
type: boolean
description: v1.5 single-administration flag. When true the medication has at most one schedule (no `rrule` /
`rollingIntervalDays`), and `active` auto-flips to false once the dose is logged.
createdAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
updatedAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
schedules:
type: array
items:
$ref: "#/components/schemas/MedicationScheduleOutput"
category:
$ref: "#/components/schemas/MedicationCategory"
required:
- id
- name
- dose
- treatmentClass
- dosesPerUnit
- active
- notificationsEnabled
- liveActivityEnabled
- criticalAlarmEnabled
- atcCode
- rxNormCode
- pausedAt
- snoozedUntil
- nextDueAt
- startsOn
- endsOn
- oneShot
- createdAt
- updatedAt
- schedules
- category
additionalProperties: false
description: Detail variant of the medication resource enriched with the joined `category`. The base medication fields
are inlined; see the `Medication` component for their semantics.
GetMedicationResponse:
type: object
properties:
data:
$ref: "#/components/schemas/MedicationDetail"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
UpdateMedicationResponse:
type: object
properties:
data:
$ref: "#/components/schemas/MedicationDetail"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
DeleteMedicationResponse:
type: object
properties:
data:
type: object
properties:
deleted:
type: boolean
required:
- deleted
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
ReplayMedicationIntakeResponse:
type: object
properties:
data:
$ref: "#/components/schemas/MedicationIntakeEvent"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
MedicationIntakeEvent:
type: object
properties:
id:
type: string
userId:
type: string
medicationId:
type: string
scheduledFor:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
takenAt:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
skipped:
type: boolean
source:
type: string
enum:
- WEB
- API
- REMINDER
- IMPORT
idempotencyKey:
anyOf:
- type: string
- type: "null"
createdAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
required:
- id
- userId
- medicationId
- scheduledFor
- takenAt
- skipped
- source
- idempotencyKey
- createdAt
additionalProperties: false
description: Single dose log row. `takenAt` is non-null for confirmed intakes; `skipped:true` represents a
deliberately-missed dose (no inventory consumption).
CreateMedicationIntakeResponse:
type: object
properties:
data:
$ref: "#/components/schemas/MedicationIntakeEvent"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
MedicationExtractResponse:
type: object
properties:
data:
type: object
properties:
name:
type: string
minLength: 1
maxLength: 100
dose:
type: string
minLength: 1
maxLength: 50
doseUnit:
type: string
enum:
- mg
- ml
- iu
- tablets
- drops
- puffs
- sprays
cadenceKind:
type: string
enum:
- daily
- weekdays
- everyNWeeks
- monthly
- everyNMonths
- yearly
- rolling
- oneShot
weekdays:
maxItems: 7
type: array
items:
type: string
enum:
- MO
- TU
- WE
- TH
- FR
- SA
- SU
intervalWeeks:
type: integer
minimum: 1
maximum: 52
intervalMonths:
type: integer
minimum: 1
maximum: 12
dayOfMonth:
type: integer
minimum: 1
maximum: 31
rollingIntervalDays:
type: integer
minimum: 1
maximum: 365
timesOfDay:
maxItems: 12
type: array
items:
type: string
pattern: ^([01]\d|2[0-3]):[0-5]\d$
startsOn:
type: string
pattern: ^\d{4}-\d{2}-\d{2}$
endsOn:
type: string
pattern: ^\d{4}-\d{2}-\d{2}$
oneShot:
type: boolean
confidence:
type: object
propertyNames:
type: string
additionalProperties:
type: number
minimum: 0
maximum: 1
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
GetMedicationCadenceResponse:
type: object
properties:
data:
$ref: "#/components/schemas/MedicationCadenceResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
MedicationCadenceResponse:
type: object
properties:
windowDays:
type: integer
exclusiveMinimum: 0
maximum: 9007199254740991
anchorIso:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
next:
anyOf:
- type: object
properties:
windowStart:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
windowEnd:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
scheduleIndex:
type: integer
minimum: 0
maximum: 9007199254740991
required:
- windowStart
- windowEnd
- scheduleIndex
additionalProperties: false
- type: "null"
chips:
$ref: "#/components/schemas/MedicationCadenceChips"
timeline:
type: array
items:
$ref: "#/components/schemas/MedicationCadenceTimelinePoint"
required:
- windowDays
- anchorIso
- next
- chips
- timeline
additionalProperties: false
description: Cadence + compliance read for a single medication. `next` is the upcoming-dose envelope (null when the
course has ended or the rolling clock has no pinning intake yet); `timeline` walks the requested `windowDays`
worth of slots in ascending time order.
MedicationCadenceChips:
type: object
properties:
adherenceRate:
type: number
streakDays:
type: integer
minimum: 0
maximum: 9007199254740991
expectedSlots:
type: integer
minimum: 0
maximum: 9007199254740991
actualDoses:
type: integer
minimum: 0
maximum: 9007199254740991
required:
- adherenceRate
- streakDays
- expectedSlots
- actualDoses
additionalProperties: false
description: Four compliance summary values for the medication detail page chip row.
MedicationCadenceTimelinePoint:
type: object
properties:
day:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
windowStart:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
windowEnd:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
scheduleIndex:
type: integer
minimum: 0
maximum: 9007199254740991
status:
type: string
required:
- day
- windowStart
- windowEnd
- scheduleIndex
- status
additionalProperties: false
description: One expected-vs-actual dose slot for the cadence timeline chart. `status` is one of `taken | skipped |
missed | pending | future` and drives the chip colour.
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.
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.
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.
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
streak:
type: integer
minimum: 0
maximum: 9007199254740991
required:
- rate
- streak
additionalProperties: false
long:
type: object
properties:
rate:
type: integer
minimum: 0
maximum: 100
required:
- rate
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.
required:
- shortDays
- longDays
- expectedShort
- expectedLong
- minStableDoses
- short
- long
- currentCycle
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.
GetCoachPrefsResponse:
type: object
properties:
data:
type: object
properties:
tone:
default: warm
type: string
enum:
- warm
- neutral
- concise
verbosity:
default: default
type: string
enum:
- brief
- default
- detailed
excludeMetrics:
default: []
maxItems: 11
type: array
items:
type: string
enum:
- bp
- weight
- pulse
- mood
- compliance
- hrv
- sleep
- resting_hr
- steps
- medications
- anthropometrics
showEvidenceByDefault:
default: false
type: boolean
defaultWindow:
default: allTime
type: string
enum:
- last7days
- last30days
- last90days
- allTime
dataClusters:
maxItems: 10
type: array
items:
type: string
enum:
- cardio
- body
- activity
- workouts
- sleep
- mood
- glucose
- medication
- mobility
- environment
required:
- tone
- verbosity
- excludeMetrics
- showEvidenceByDefault
- defaultWindow
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
PutCoachPrefsResponse:
type: object
properties:
data:
type: object
properties:
tone:
default: warm
type: string
enum:
- warm
- neutral
- concise
verbosity:
default: default
type: string
enum:
- brief
- default
- detailed
excludeMetrics:
default: []
maxItems: 11
type: array
items:
type: string
enum:
- bp
- weight
- pulse
- mood
- compliance
- hrv
- sleep
- resting_hr
- steps
- medications
- anthropometrics
showEvidenceByDefault:
default: false
type: boolean
defaultWindow:
default: allTime
type: string
enum:
- last7days
- last30days
- last90days
- allTime
dataClusters:
maxItems: 10
type: array
items:
type: string
enum:
- cardio
- body
- activity
- workouts
- sleep
- mood
- glucose
- medication
- mobility
- environment
required:
- tone
- verbosity
- excludeMetrics
- showEvidenceByDefault
- defaultWindow
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
GetDisableCoachResponse:
type: object
properties:
data:
$ref: "#/components/schemas/DisableCoachFlagOutput"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
PatchDisableCoachResponse:
type: object
properties:
data:
$ref: "#/components/schemas/DisableCoachFlagOutput"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
GetSourcePriorityResponse:
type: object
properties:
data:
type: object
properties:
steps:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
activeEnergy:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
walkingRunningDistance:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
flightsClimbed:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
sleep:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
weight:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
bloodPressure:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
pulse:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
bodyFat:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
bodyTemperature:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
spo2:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
hrv:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
restingHeartRate:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
vo2Max:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
skinTemperature:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
respiratoryRate:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
recovery:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
metricPriority:
type: object
properties:
steps:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
activeEnergy:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
walkingRunningDistance:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
flightsClimbed:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
sleep:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
weight:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
bloodPressure:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
pulse:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
bodyFat:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
bodyTemperature:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
spo2:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
hrv:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
restingHeartRate:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
vo2Max:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
skinTemperature:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
respiratoryRate:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
recovery:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
additionalProperties: false
deviceTypePriority:
type: object
properties:
default:
maxItems: 8
type: array
items:
type: string
enum:
- watch
- band
- ring
- phone
- scale
- other
- unknown
additionalProperties:
maxItems: 8
type: array
items:
type: string
enum:
- watch
- band
- ring
- phone
- scale
- other
- unknown
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
PutSourcePriorityResponse:
type: object
properties:
data:
type: object
properties:
steps:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
activeEnergy:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
walkingRunningDistance:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
flightsClimbed:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
sleep:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
weight:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
bloodPressure:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
pulse:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
bodyFat:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
bodyTemperature:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
spo2:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
hrv:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
restingHeartRate:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
vo2Max:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
skinTemperature:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
respiratoryRate:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
recovery:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
metricPriority:
type: object
properties:
steps:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
activeEnergy:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
walkingRunningDistance:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
flightsClimbed:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
sleep:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
weight:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
bloodPressure:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
pulse:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
bodyFat:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
bodyTemperature:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
spo2:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
hrv:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
restingHeartRate:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
vo2Max:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
skinTemperature:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
respiratoryRate:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
recovery:
maxItems: 8
type: array
items:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
additionalProperties: false
deviceTypePriority:
type: object
properties:
default:
maxItems: 8
type: array
items:
type: string
enum:
- watch
- band
- ring
- phone
- scale
- other
- unknown
additionalProperties:
maxItems: 8
type: array
items:
type: string
enum:
- watch
- band
- ring
- phone
- scale
- other
- unknown
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
UploadAvatarResponse:
type: object
properties:
data:
type: object
properties:
avatarUrl:
type: string
contentType:
type: string
enum:
- image/jpeg
- image/png
- image/webp
updatedAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
required:
- avatarUrl
- contentType
- updatedAt
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
GetProfileResponse:
type: object
properties:
data:
$ref: "#/components/schemas/ProfileResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
ProfileResponse:
type: object
properties:
username:
type: string
displayName:
anyOf:
- type: string
- type: "null"
email:
anyOf:
- type: string
- type: "null"
dateOfBirth:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
gender:
anyOf:
- type: string
enum:
- MALE
- FEMALE
- type: "null"
heightCm:
anyOf:
- type: number
- type: "null"
locale:
anyOf:
- type: string
- type: "null"
timezone:
type: string
moodReminderEnabled:
type: boolean
fullName:
anyOf:
- type: string
- type: "null"
insurerName:
anyOf:
- type: string
- type: "null"
insurerIkNumber:
anyOf:
- type: string
- type: "null"
description: German insurer institution number (IKNR), 9 digits.
insuranceNumber:
anyOf:
- type: string
- type: "null"
description: German KVNR, decrypted for the form prefill.
required:
- username
- email
- dateOfBirth
- gender
- heightCm
- locale
- timezone
- moodReminderEnabled
- fullName
- insurerName
- insurerIkNumber
- insuranceNumber
additionalProperties: false
description: Flattened profile fields for the native client (GET). The KVNR is decrypted server-side; the IKNR (v1.8.6)
is plaintext at rest.
PatchProfileResponse:
type: object
properties:
data:
$ref: "#/components/schemas/ProfileUpdateResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
ProfileUpdateResponse:
type: object
properties:
username:
type: string
displayName:
anyOf:
- type: string
- type: "null"
email:
anyOf:
- type: string
- type: "null"
dateOfBirth:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
gender:
anyOf:
- type: string
enum:
- MALE
- FEMALE
- type: "null"
heightCm:
anyOf:
- type: number
- type: "null"
locale:
anyOf:
- type: string
- type: "null"
timezone:
type: string
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
- 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.
InsightsLayout:
type: object
properties:
data:
$ref: "#/components/schemas/InsightsLayoutBodyOutput"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
InsightsLayoutSaved:
type: object
properties:
data:
$ref: "#/components/schemas/InsightsLayoutBodyOutput"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
InsightsLayoutReset:
type: object
properties:
data:
$ref: "#/components/schemas/InsightsLayoutBodyOutput"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
CoachFactsList:
type: object
properties:
data:
type: object
properties:
facts:
type: array
items:
type: object
properties:
id:
type: string
category:
type: string
enum:
- preference
- condition
- goal
- constraint
- context
description: "App-side closed category: preference | condition | goal | constraint | context."
text:
type: string
description: Decrypted fact text.
confidence:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
description: 0..100 server-assigned extraction confidence.
createdAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
required:
- id
- category
- text
- confidence
- createdAt
additionalProperties: false
description: The caller's active facts, highest-confidence then newest first. Undecryptable rows are omitted.
required:
- facts
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
CoachFactsCleared:
type: object
properties:
data:
type: object
properties:
cleared:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
description: Number of active facts soft-deleted by the bulk clear.
required:
- cleared
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
CoachFactDeleted:
type: object
properties:
data:
type: object
properties:
deleted:
type: boolean
description: True when a fact owned by the caller was soft-deleted; false for an unknown / cross-user / already-deleted
id (idempotent no-op).
required:
- deleted
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
CoachMessageFeedbackResponse:
type: object
properties:
data:
type: object
properties:
id:
type: string
createdAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
required:
- id
- createdAt
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
AdminDiagnosticResponse:
type: object
properties:
data:
$ref: "#/components/schemas/AdminDiagnosticData"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
AdminDiagnosticData:
type: object
properties:
devices:
type: array
items:
$ref: "#/components/schemas/AdminDiagnosticDevice"
notificationChannels:
type: array
items:
$ref: "#/components/schemas/AdminDiagnosticChannel"
recentPushAttempts:
type: array
items:
$ref: "#/components/schemas/AdminDiagnosticPushAttempt"
required:
- devices
- notificationChannels
- recentPushAttempts
additionalProperties: false
description: Admin notification diagnostic snapshot for the calling user — what the dispatcher would see when targeting
this account. Surfaces device tokens (masked), channel state, and recent push attempts so an operator can debug
an iOS / Web Push / Telegram / ntfy issue without DB shell access.
AdminDiagnosticDevice:
type: object
properties:
id:
type: string
platform:
type: string
hasApnsToken:
type: boolean
apnsTokenPrefix:
anyOf:
- type: string
- type: "null"
apnsTokenSuffix:
anyOf:
- type: string
- type: "null"
apnsEnvironment:
anyOf:
- type: string
- type: "null"
lastSeenAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
required:
- id
- platform
- hasApnsToken
- apnsTokenPrefix
- apnsTokenSuffix
- apnsEnvironment
- lastSeenAt
additionalProperties: false
description: Per-device APNs registration snapshot. The APNs token is masked to its 8-char hex prefix + 8-char suffix so
an operator can correlate with iOS-side logs without disclosing the delivery target.
AdminDiagnosticChannel:
type: object
properties:
type:
type: string
enabled:
type: boolean
configPresent:
type: boolean
required:
- type
- enabled
- configPresent
additionalProperties: false
description: Per-channel state. `configPresent` is true when the channel's encrypted-JSON config blob carries the field
the corresponding sender will read (chatId+botToken for TELEGRAM, topic for NTFY); WEB_PUSH/APNS rely on sibling
tables so the row's existence is the signal.
AdminDiagnosticPushAttempt:
type: object
properties:
eventType:
type: string
channel:
type: string
result:
type: string
reason:
anyOf:
- type: string
- type: "null"
at:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
required:
- eventType
- channel
- result
- reason
- at
additionalProperties: false
description: Single push delivery attempt. Reserved for the v1.4.48+ follow-up that adds the dedicated PushAttempt table
— currently always returned as an empty array so the response shape stays stable for iOS consumers.
DashboardSnapshotResponseEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/DashboardSnapshotResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
DashboardSnapshotResponse:
type: object
properties:
user:
type: object
properties:
username:
type: string
timezone:
type: string
heightCm:
anyOf:
- type: number
- type: "null"
dateOfBirth:
anyOf:
- type: string
- type: "null"
gender:
anyOf:
- type: string
enum:
- MALE
- FEMALE
- type: "null"
glucoseUnit:
anyOf:
- type: string
- type: "null"
onboardingTourCompleted:
type: boolean
greetingHour:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
required:
- username
- timezone
- heightCm
- dateOfBirth
- gender
- glucoseUnit
- onboardingTourCompleted
- greetingHour
additionalProperties: false
layout:
type: object
propertyNames:
type: string
additionalProperties: {}
layoutCatalogue:
type: array
items:
type: object
properties:
id:
type: string
visible:
type: boolean
order:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
required:
- id
- visible
- order
additionalProperties: false
description: Full 27-id widget catalogue (server-known + iOS-only) with per-widget visibility + order. iOS-only ids are
appended default-invisible. The web dashboard reads `layout`; this block is the cold-launch seed for the
native client.
metricStates:
type: object
propertyNames:
type: string
additionalProperties:
type: object
properties:
value:
type: number
measuredAt:
type: string
unit:
type: string
required:
- value
- measuredAt
- unit
additionalProperties: false
description: "Latest reading per chartable metric, keyed by the iOS `MetricKind` raw value (the non-obvious raws:
`oxygenSaturation`, `totalBodyWater`, `heartRateVariability`, `bodyMassIndex`, `walkingAsymmetryPercentage`,
`walkingDoubleSupportPercentage`, `environmentalAudioExposure`, `headphoneAudioExposure`,
`activeEnergyBurned`). Each entry carries `value`, `measuredAt` (ISO8601), and the canonical `unit`. Types
the user has never logged are omitted."
tiles:
type: object
properties:
summaries:
type: object
propertyNames:
type: string
additionalProperties: {}
lastSeenByType:
type: object
propertyNames:
type: string
additionalProperties: {}
mood:
type: object
properties:
summary:
anyOf:
- type: object
propertyNames:
type: string
additionalProperties: {}
- type: "null"
entries:
type: array
items:
type: object
properties:
date:
type: string
score:
type: number
samples:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
required:
- date
- score
- samples
additionalProperties: false
required:
- summary
- entries
additionalProperties: false
required:
- summaries
- lastSeenByType
- mood
additionalProperties: false
extras:
anyOf:
- type: object
properties:
bpInTargetPct:
anyOf:
- type: number
- type: "null"
bpInTargetPct7d:
anyOf:
- type: number
- type: "null"
bpInTargetPct30d:
anyOf:
- type: number
- type: "null"
bpInTargetPctAllTime:
anyOf:
- type: number
- type: "null"
bpInTargetPctPriorMonth:
anyOf:
- type: number
- type: "null"
bpInTargetPctPriorYear:
anyOf:
- type: number
- type: "null"
glucoseByContext:
type: object
propertyNames:
type: string
additionalProperties: {}
required:
- bpInTargetPct
- bpInTargetPct7d
- bpInTargetPct30d
- bpInTargetPctAllTime
- bpInTargetPctPriorMonth
- bpInTargetPctPriorYear
- glucoseByContext
additionalProperties: false
- type: "null"
briefing:
anyOf:
- type: object
propertyNames:
type: string
additionalProperties: {}
- type: "null"
briefingState:
type: string
enum:
- ready
- preparing
- disabled
briefingUpdatedAt:
anyOf:
- type: string
- type: "null"
generatedAt:
type: string
required:
- user
- layout
- layoutCatalogue
- metricStates
- tiles
- extras
- briefing
- briefingState
- briefingUpdatedAt
- generatedAt
additionalProperties: false
description: Unified above-the-fold dashboard payload. `tiles` always arrives (slim summaries + mood + resolved widget
layout); `extras` (BD-in-target + per-context glucose) is null on a rollup-coverage miss so the strip never
waits on the slowest read. `briefing` is lifted read-only from the pre-generated insight cache — never generated
synchronously — and reports `ready` / `preparing` / `disabled` via `briefingState`. `layoutCatalogue` (full
27-id widget catalogue) and `metricStates` (latest reading per metric, keyed by iOS `MetricKind` raw value) are
additive cold-launch seeds for the native client; both derive in-process from data already fetched, adding no DB
round-trip.
InsightsComprehensiveResponseEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/InsightsComprehensiveResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
InsightsComprehensiveResponse:
type: object
properties:
summary:
type: string
recommendations:
type: array
items:
type: object
propertyNames:
type: string
additionalProperties: {}
citations:
type: array
items:
type: object
propertyNames:
type: string
additionalProperties: {}
warnings:
type: array
items:
type: object
propertyNames:
type: string
additionalProperties: {}
dailyBriefing:
anyOf:
- type: object
propertyNames:
type: string
additionalProperties: {}
- type: "null"
trendAnnotations:
anyOf:
- type: object
propertyNames:
type: string
additionalProperties: {}
- type: "null"
storyboardAnnotations:
type: array
items:
type: object
propertyNames:
type: string
additionalProperties: {}
metricSource:
type: object
propertyNames:
type: string
additionalProperties: {}
required:
- summary
- recommendations
- citations
- warnings
additionalProperties: false
description: AI-generated insights bundle. Strict-schema validated server-side; Coach-routed when the insight surface
needs day-level grounding.
InsightsPregenerateResponseEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/InsightsPregenerateResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
InsightsPregenerateResponse:
type: object
properties:
queued:
type: boolean
description: True when the full warm was accepted and enqueued.
locale:
type: string
enum:
- de
- en
description: The locale the assessments are being warmed in.
required:
- queued
- locale
additionalProperties: false
description: Acknowledgement that a full assessment warm was enqueued for the calling user. The generation runs out of
band; the text lands in the read-only status routes.
AnalyticsRangeResponseEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/AnalyticsRangeResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
AnalyticsRangeResponse:
type: object
properties:
range:
type: string
enum:
- 7d
- 30d
- 90d
- 1y
description: The range that was read (echoes the request).
windowDays:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
description: Trailing-window length in days for the chosen range.
granularity:
type: string
description: Rollup granularity the read resolved against (`DAY` / `WEEK` / `MONTH` / `YEAR`, or `none` on a coverage
miss).
current:
description: Aggregate over the current window `[now-N, now)`.
$ref: "#/components/schemas/AnalyticsWindowAggregate"
previous:
description: Aggregate over the previous comparable window `[now-2N, now-N)`.
$ref: "#/components/schemas/AnalyticsWindowAggregate"
delta:
anyOf:
- type: number
- type: "null"
description: "`current.mean - previous.mean`; null when either window has no data (never a misleading 0)."
deltaPct:
anyOf:
- type: number
- type: "null"
description: "`delta / previous.mean` as a fraction (0.03 = +3 %); null when the prior window has no / zero mean (no
divide-by-zero). The client shows 'no prior-period data' in that case."
required:
- range
- windowDays
- granularity
- current
- previous
- delta
- deltaPct
additionalProperties: false
description: Single-metric period-over-period aggregate. Reads the current and previous comparable windows from the WMY
rollup tier and composes a count-weighted-mean delta. `count/min/max/mean/sum` are linearly composable across
buckets; SD/slope/r² are intentionally excluded (not composable).
AnalyticsWindowAggregate:
type: object
properties:
count:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
description: Reading count composed across buckets.
min:
anyOf:
- type: number
- type: "null"
description: Window minimum; null when empty.
max:
anyOf:
- type: number
- type: "null"
description: Window maximum; null when empty.
mean:
anyOf:
- type: number
- type: "null"
description: Count-weighted mean across buckets; null when empty.
sum:
anyOf:
- type: number
- type: "null"
description: Cumulative total for cumulative metrics (steps, energy, distance); null when no bucket carries a sum.
required:
- count
- min
- max
- mean
- sum
additionalProperties: false
BloodPressureStatusResponseEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/InsightStatusResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
InsightStatusResponse:
type: object
properties:
hasProvider:
type: boolean
description: False when the user has no usable AI provider — `text` then carries the generic no-key guidance.
text:
anyOf:
- type: string
- type: "null"
description: The assessment narrative (plain text, rendered as React text children). Null while a first generation is
preparing.
cached:
type: boolean
description: True when `text` is served from cache (incl. last-good).
updatedAt:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
description: When the served assessment was generated; null when none.
preparing:
description: True when a first assessment is being generated out of band and no prior text exists yet — the client polls
until it lands.
type: boolean
revalidating:
description: True when `text` is served from last-good cache (stale-while-revalidate) while a fresh generation is in
flight. The client keeps polling on `preparing || revalidating` (bounded) so the open card upgrades to the
warmed assessment without a remount.
type: boolean
required:
- hasProvider
- text
- cached
- updatedAt
additionalProperties: false
description: "Specialised per-metric assessment envelope (blood-pressure, pulse, weight, bmi, mood). Identical shape to
the generic metric-status card so the `InsightStatusCard` consumes it unchanged. Read-only +
stale-while-revalidate: a cache miss warms a generation out of band and serves the last-good text meanwhile."
PulseStatusResponseEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/InsightStatusResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
WeightStatusResponseEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/InsightStatusResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
BmiStatusResponseEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/InsightStatusResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
MoodStatusResponseEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/InsightStatusResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
MedicationComplianceStatusResponseEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/MedicationComplianceStatusResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
MedicationComplianceStatusResponse:
type: object
properties:
hasProvider:
type: boolean
description: False when the user has no usable AI provider — `summary` then carries the generic no-key guidance.
summary:
anyOf:
- type: string
- type: "null"
description: The overall compliance narrative (plain text). Null while a first generation is preparing.
medications:
type: array
items:
$ref: "#/components/schemas/MedicationComplianceStatusItem"
description: Per-medication compliance notes. Empty while preparing or when no medication qualifies.
cached:
type: boolean
description: True when the envelope is served from cache (incl. last-good).
updatedAt:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
description: When the served assessment was generated; null when none.
preparing:
description: True when a first assessment is being generated out of band and no prior summary exists yet — the client
polls until it lands.
type: boolean
revalidating:
description: True when the envelope is served from last-good cache (stale-while-revalidate) while a fresh generation is
in flight. The client keeps polling on `preparing || revalidating` (bounded).
type: boolean
required:
- hasProvider
- summary
- medications
- cached
- updatedAt
additionalProperties: false
description: Medication-compliance assessment envelope. Unlike the other six specialised cards it carries a `summary`
plus a per-medication `text` array rather than a single `text` field. Read-only + stale-while-revalidate.
MedicationComplianceStatusItem:
type: object
properties:
medicationId:
type: string
description: The medication this note belongs to.
text:
type: string
description: Per-medication compliance note (plain text).
required:
- medicationId
- text
additionalProperties: false
MetricStatusResponseEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/MetricStatusResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
MetricStatusResponse:
type: object
properties:
hasProvider:
type: boolean
description: False when the user has no usable AI provider — `text` then carries the generic no-key guidance.
text:
anyOf:
- type: string
- type: "null"
description: The assessment narrative (plain text, rendered as React text children). Null while a first generation is
preparing, or when the metric has insufficient data.
cached:
type: boolean
description: True when `text` is served from cache (incl. last-good).
updatedAt:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
description: When the served assessment was generated; null when none.
preparing:
description: True when a first assessment is being generated out of band and no prior text exists yet — the client polls
until it lands.
type: boolean
revalidating:
description: True when `text` is served from last-good cache (stale-while-revalidate) while a fresh generation is in
flight. The payload is otherwise terminal; the client keeps polling on `preparing || revalidating` (bounded)
so the open card upgrades to the warmed assessment without a remount.
type: boolean
insufficient:
description: True when the metric has no readings; no assessment is generated (no LLM call). The card shows its
insufficient-data state.
type: boolean
required:
- hasProvider
- text
- cached
- updatedAt
additionalProperties: false
description: "Generic per-metric assessment envelope. Identical shape to the seven specialised `*-status` cards so the
`InsightStatusCard` consumes it unchanged. Read-only + stale-while-revalidate: a cache miss warms a generation
out of band and serves the last-good text meanwhile."
DerivedMetricResponseEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/DerivedMetricResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
DerivedMetricResponse:
type: object
properties:
metric:
type: string
enum:
- VITALS_BASELINE
- FITNESS_AGE
- VASCULAR_AGE_DELTA
- SLEEP_SCORE
- READINESS
- COINCIDENT_DEVIATION
- HRV_BALANCE
- BMI
- RECOVERY_SCORE
- STRESS_SCORE
- STRAIN_SCORE
- WRIST_TEMPERATURE_BASELINE
- STAIR_ASCENT_SPEED_BASELINE
- STAIR_DESCENT_SPEED_BASELINE
- SIX_MINUTE_WALK_BAND
- TRAJECTORY
description: Echoes the requested derived-metric id (tags the union).
status:
type: string
enum:
- ok
- insufficient
description: "'ok' carries `value` + `confidence`; 'insufficient' carries `reason` and no value, but still carries
`coverage` + `provenance` so the surface renders the same gating UI."
value:
anyOf:
- type: object
propertyNames:
type: string
additionalProperties: {}
- type: "null"
description: Metric-specific value object when status is 'ok' (e.g. { type, center, low, high, spread, sampleDays, k,
series } for VITALS_BASELINE, where `series` is the trailing per-day mean values for the inline sparkline);
null when 'insufficient'.
coverage:
$ref: "#/components/schemas/DerivedCoverage"
confidence:
anyOf:
- $ref: "#/components/schemas/DerivedConfidence"
- type: "null"
description: Present when status is 'ok'; null when 'insufficient'.
provenance:
$ref: "#/components/schemas/DerivedProvenance"
reason:
anyOf:
- type: string
- type: "null"
description: Why the value could not be produced; null when status is 'ok'.
assessment:
anyOf:
- $ref: "#/components/schemas/DerivedAssessment"
- type: "null"
description: "v1.13.2 — short 'why is this score what it is' explanation, keyed to the SAME requested id (only for the
per-score ids READINESS, SLEEP_SCORE, RECOVERY_SCORE, STRAIN_SCORE, STRESS_SCORE). Null for any other metric
and whenever status !== 'ok'. Always non-empty when present: a deterministic text fills it (so provider-less
accounts + the demo always get one) and warmer AI prose overrides it once cached."
required:
- metric
- status
- value
- coverage
- confidence
- provenance
- reason
- assessment
additionalProperties: false
description: Flat `Derived<T>` envelope for one derived wellness metric. Pure compute over the rollup tier (no LLM, no
narrative). iOS decodes one stable shape and combines values across metrics; coverage/confidence/provenance let
it render the same honesty chips.
DerivedCoverage:
type: object
properties:
requiredInputs:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
description: Inputs the metric wants (its full input set).
presentInputs:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
description: Inputs actually present in the user's data.
historyDays:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
description: Distinct days of history backing the value (the gating floor).
missing:
type: array
items:
type: string
description: Named inputs still missing — drives the 'track N more' nudge.
required:
- requiredInputs
- presentInputs
- historyDays
- missing
additionalProperties: false
DerivedConfidence:
type: object
properties:
score:
type: number
description: 0..100 confidence; feeds the shared coverage meter unchanged.
band:
type: string
enum:
- high
- medium
- low
- draft
description: Confidence band the meter renders.
required:
- score
- band
additionalProperties: false
DerivedProvenance:
type: object
properties:
inputs:
type: array
items:
type: string
description: Named inputs that actually backed the value.
source:
type: string
enum:
- DAY
- WEEK
- MONTH
- YEAR
- live
- none
description: Granularity the dominant read resolved against. 'live' = a coverage-miss live-SQL fallback; 'none' = no
data backed the value.
windowDays:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
description: Trailing window the value summarises, in days.
computedAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
description: Compute time (for cache-staleness + the 'as of' chip).
required:
- inputs
- source
- windowDays
- computedAt
additionalProperties: false
DerivedAssessment:
type: object
properties:
text:
type: string
description: Short, non-empty explanation of why the score sits where it does, referencing the score's contributors.
source:
type: string
description: "'deterministic' for the always-on template text, or 'ai' when warmer provider prose has been cached."
updatedAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
description: When the text was produced / last warmed.
required:
- text
- source
- updatedAt
additionalProperties: false
DerivedBatchResponseEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/DerivedBatchResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
DerivedBatchResponse:
type: object
properties:
metrics:
type: object
propertyNames:
type: string
additionalProperties:
$ref: "#/components/schemas/DerivedMetricResponse"
description: Map keyed by the per-request token (`<metric>` or `<metric>:<type>`). Each value is the same flat
`Derived<T>` envelope the single-metric route returns, so a client decodes one shape and reads back exactly
the tokens it asked for.
required:
- metrics
additionalProperties: false
description: Batched derived-metric values. One request resolves the whole dashboard grid (the wellness scores + the
derived re-frames + one baseline per vital) instead of N concurrent single-metric requests sharing the Prisma
pool. Pure compute over the rollup tier — no LLM, no narrative, no cache table.
CorrelationDiscoveryResponseEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/CorrelationDiscoveryResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
CorrelationDiscoveryResponse:
type: object
properties:
discovered:
type: array
items:
$ref: "#/components/schemas/DiscoveredCorrelation"
description: Pairs surviving n ≥ 20, p < 0.05, AND the BH-FDR control.
pairsTested:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
description: Behaviour × outcome pairs assessed (for the honest footer).
fdrQ:
type: number
description: The FDR target the surface used.
minPairs:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
description: Minimum paired-day count enforced per pair.
required:
- discovered
- pairsTested
- fdrQ
- minPairs
additionalProperties: false
description: v1.10.0 — FDR-controlled correlation discovery over a curated behaviour × outcome matrix, lagged behaviour
→ next-day outcome. Only statistically-defensible pairs surface; descriptive, never causal.
DiscoveredCorrelation:
type: object
properties:
behaviour:
type: string
description: Behaviour channel (lag source), e.g. TIME_IN_DAYLIGHT, MOOD.
outcome:
type: string
description: Outcome channel (lag target), e.g. SLEEP_DURATION, HEART_RATE_VARIABILITY.
n:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
description: Paired-day count after the day+1 lag join (≥ 20).
r:
type: number
description: Pearson r over the lag-joined daily series.
pValue:
type: number
description: Two-sided exact Student-t p-value (< 0.05).
qValue:
type: number
description: Benjamini-Hochberg FDR-adjusted q-value (≤ the surface threshold).
interpretation:
type: string
description: Conservative, descriptive interpretation — never causal.
lagDays:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
description: Lag in days applied (1).
required:
- behaviour
- outcome
- n
- r
- pValue
- qValue
- interpretation
- lagDays
additionalProperties: false
MedicationScheduleOutput:
type: object
properties:
id:
type: string
medicationId:
type: string
windowStart:
type: string
description: Legacy single-time-of-intake (HH:mm, user local). Preserved for backwards compatibility; the new
`timesOfDay` array supersedes it.
windowEnd:
type: string
description: Legacy reminder-window upper bound (HH:mm). Used to derive the late-classification grace span when
`reminderGraceMinutes` is null.
label:
anyOf:
- type: string
- type: "null"
dose:
anyOf:
- type: string
- type: "null"
description: Per-schedule dose override. NULL means the schedule inherits `Medication.dose`.
daysOfWeek:
anyOf:
- type: string
- type: "null"
description: Legacy persisted recurrence encoding (`null` | `1,3,5` | `i2;1,3,5`). v1.5 readers consult `rrule` first;
the field is kept for pre-v1.5 rows. v1.6.0 drops the column.
timesOfDay:
type: array
items:
type: string
description: v1.5 first-class points-in-time the dose is taken (HH:mm, user local). Backfilled to `[windowStart]` for
every pre-v1.5 row.
reminderGraceMinutes:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
description: Reminder grace window in minutes. NULL falls back to the legacy `windowEnd - windowStart` span.
rrule:
anyOf:
- type: string
- type: "null"
description: RFC 5545 RRULE string (subset). Used for calendar-anchored cadences. **Mutually exclusive with
`rollingIntervalDays`** — exactly one of the two is non-null on any v1.5+ schedule (or both are null on
legacy rows that haven't been touched since the migration).
rollingIntervalDays:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
description: Flexible-rolling interval in days, counted forward from the latest `MedicationIntakeEvent.takenAt`.
**Mutually exclusive with `rrule`.**
scheduleType:
type: string
enum:
- SCHEDULED
- PRN
- CYCLIC
description: v1.7.0 schedule-type discriminator. SCHEDULED = rrule / rolling / legacy cadence. PRN = as-needed (never
projected, reminded, or counted in compliance expected; still loggable via the intake route). CYCLIC = N
weeks on / M weeks off, gating whichever inner cadence the rrule / legacy fields describe.
cyclicOnWeeks:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
description: v1.7.0 cyclic "on" weeks. Only meaningful when `scheduleType` is CYCLIC; null otherwise.
cyclicOffWeeks:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
description: v1.7.0 cyclic "off" weeks. Only meaningful when `scheduleType` is CYCLIC; null otherwise.
required:
- id
- medicationId
- windowStart
- windowEnd
- label
- dose
- daysOfWeek
- timesOfDay
- reminderGraceMinutes
- rrule
- rollingIntervalDays
- scheduleType
- cyclicOnWeeks
- cyclicOffWeeks
additionalProperties: false
description: Schedule entry attached to a medication. v1.5 promotes `timesOfDay` to first-class and introduces `rrule`
(calendar-anchored cadences) and `rollingIntervalDays` (flexible-rolling cadences). The two recurrence
primitives are mutually exclusive — enforced by the Zod refine on writes, the route layer, and a DB CHECK
constraint (`medication_schedules_rrule_xor_rolling`). v1.7.0 adds `scheduleType` (SCHEDULED / PRN / CYCLIC) and
the cyclic on/off-week fields.
DisableCoachFlagOutput:
type: object
properties:
disableCoach:
type: boolean
required:
- disableCoach
additionalProperties: false
description: Per-account Coach opt-out toggle (v1.4.47 W3). `true` hides the Coach FAB and short-circuits its API gates.
InsightsLayoutBodyOutput:
type: object
properties:
version:
type: number
const: 1
tiles:
minItems: 1
maxItems: 50
type: array
items:
type: object
properties:
id:
type: string
enum:
- overview
- blood-pressure
- pulse
- oxygen
- body-temperature
- respiratory-rate
- weight
- bmi
- body-water
- bone-mass
- fat-free-mass
- fat-mass
- muscle-mass
- visceral-fat
- lean-body-mass
- steps
- active-energy
- workouts
- flights-climbed
- walking-distance
- walking-steadiness
- walking-heart-rate
- walking-asymmetry
- double-support-time
- step-length
- walking-speed
- cardio-fitness
- falls
- six-minute-walk
- stair-ascent-speed
- stair-descent-speed
- sleep
- breathing-disturbances
- resting-pulse
- hrv
- pulse-wave-velocity
- vascular-age
- cardio-recovery
- environmental-audio
- headphone-audio
- audio-events
- daylight
- blood-glucose
- skin-temperature
- wrist-temperature
- mood
- medications
- blutdruck
- puls
- sauerstoff
- koerpertemperatur
- gewicht
- aktive-energie
- schlaf
- ruhepuls
- stimmung
- medikamente
visible:
type: boolean
order:
type: integer
minimum: 0
maximum: 99
required:
- id
- visible
- order
additionalProperties: false
required:
- version
- tiles
additionalProperties: false
description: "Per-user Insights tile layout: an ordered list of tiles with a visibility flag. `version` is the layout
schema version. Tile ids are a closed enum: the canonical ids are English (matching the routed
`/insights/<slug>` sub-pages). The legacy German ids (blutdruck, puls, sauerstoff, koerpertemperatur, gewicht,
aktive-energie, schlaf, ruhepuls, stimmung, medikamente) remain accepted on input for backward compatibility and
are normalised to their English equivalents before persisting; GET responses always carry the canonical English
ids. The legacy ids are deprecated and will be removed in a future major version."
Medication:
type: object
properties:
id:
type: string
name:
type: string
dose:
type: string
treatmentClass:
$ref: "#/components/schemas/MedicationTreatmentClass"
dosesPerUnit:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
description: Doses per pen / vial for inventory tracking. NULL = inventory tracking off.
active:
type: boolean
notificationsEnabled:
type: boolean
liveActivityEnabled:
type: boolean
description: v1.7.0 iOS Live Activity opt-in for this medication's reminders. Default false. The iOS client owns the
ActivityKit lifecycle; the server only stores + echoes the flag.
criticalAlarmEnabled:
type: boolean
description: v1.7.0 iOS 26 AlarmKit critical-reminder opt-in. Default false. Critical alarms bypass the device mute
switch / Focus; the server stores the preference only.
atcCode:
anyOf:
- type: string
- type: "null"
description: v1.9.0 optional WHO ATC classification code (active-substance class, e.g. `A10BX10`).
User/clinician-asserted; never machine-guessed. Emitted on the FHIR `medicationCodeableConcept` under
`http://www.whocc.no/atc`. NULL = no code captured.
rxNormCode:
anyOf:
- type: string
- type: "null"
description: v1.9.0 optional RxNorm RxCUI (numeric, US identifier, e.g. `2601723`). Secondary FHIR coding under
`http://www.nlm.nih.gov/research/umls/rxnorm`, alongside any ATC code. NULL = no code captured.
pausedAt:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
snoozedUntil:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
nextDueAt:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
description: v1.7.0 server-computed next due instant across all the medication's schedules (earliest
`nextOccurrenceAfter`). Read-only — computed, not stored. NULL when no schedule has an upcoming slot
(paused, one-shot in the past, `endsOn` crossed, every schedule PRN). The list GET is cached 60 s, so a 60 s
staleness is accepted.
startsOn:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
description: v1.5 course start (ISO date). Anchors RRULE BYDAY / BYMONTHDAY patterns and the rolling-interval
countdown's first window. NULL means active from creation.
endsOn:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
description: v1.5 course end (ISO date). NULL means chronic. Equals `startsOn` when `oneShot` is true.
oneShot:
type: boolean
description: v1.5 single-administration flag. When true the medication has at most one schedule (no `rrule` /
`rollingIntervalDays`), and `active` auto-flips to false once the dose is logged.
createdAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
updatedAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
schedules:
type: array
items:
$ref: "#/components/schemas/MedicationScheduleOutput"
required:
- id
- name
- dose
- treatmentClass
- dosesPerUnit
- active
- notificationsEnabled
- liveActivityEnabled
- criticalAlarmEnabled
- atcCode
- rxNormCode
- pausedAt
- snoozedUntil
- nextDueAt
- startsOn
- endsOn
- oneShot
- createdAt
- updatedAt
- schedules
additionalProperties: false
description: Server-shaped medication row returned by GET / POST / PUT endpoints. Carries the v1.5 course-window fields
(`startsOn`, `endsOn`, `oneShot`) at the medication level and the per-schedule cadence fields on the nested
`schedules` array.
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: hlk_<64hex>
description: Native-client API token. Use the `token` field returned by /api/auth/login on a native client.
cookieAuth:
type: apiKey
in: cookie
name: healthlog_session
description: Browser session cookie — set by /api/auth/login.