Files
HealthLog/docs/api/openapi.yaml
T
Marc-André BombeckandClaude Opus 4.7 75b52b1f30 chore(api): add OpenAPI 3.1 spec
Generates `docs/api/openapi.yaml` covering the full REST surface — 100+
paths across 24 tags, 130+ schemas under `components.schemas`. Two
security schemes: `sessionCookie` (HttpOnly `healthlog_session`) plus
`bearerAuth` (HTTP Bearer for the API tokens at `/api/tokens`). Admin
routes are explicitly tagged + restricted to `sessionCookie`.

`docs/api/README.md` documents how to preview the spec locally
(`npx @redocly/cli preview docs/api/openapi.yaml`) and lists the few
known lint warnings (localhost server, redirect endpoints without 2xx).
Lint pass: 0 errors, 17 warnings (none introduced by this commit).

Spec is hand-maintained for now; a future commit can wire it into a
`pnpm openapi:lint` CI step.

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

4961 lines
164 KiB
YAML

openapi: 3.1.0
info:
title: HealthLog API
version: 1.3.0
description: |
REST API for HealthLog — a personal health-tracking PWA covering weight,
blood pressure, pulse, mood, medication compliance, blood glucose, and
sleep. The same API powers the Next.js web client and the native iOS
client.
## Authentication
All non-public endpoints accept either a session cookie (`healthlog_session`,
obtained via password login or passkey verification) or a long-lived Bearer
token (`Authorization: Bearer hlk_...`) issued via `/api/tokens`. The two
schemes are interchangeable on user endpoints — clients pick whichever
suits them (cookies for the web app, Bearer for the iOS client and
machine-to-machine integrations). Admin endpoints (`/api/admin/*`) accept
only the session cookie; Bearer tokens never elevate to admin.
The API uses an envelope response shape: `{ data, error }`. On success,
`data` carries the payload and `error` is `null`. On failure, `data` is
`null` and `error` is a human-readable message.
## Conventions
- All timestamps are ISO 8601 (`date-time`) in UTC.
- All numeric IDs are CUIDs (opaque strings).
- Pagination uses `limit` + `offset` query parameters; responses include
a `meta: { total, limit, offset }` block.
- Rate limiting is applied per-IP or per-user on auth, ingest, export,
AI, and bug-report endpoints. Exceeding the budget yields a `429`.
## Admin endpoints
Routes under `/api/admin/*` are tagged `Admin` and are intended for the
internal admin UI only. iOS clients should not consume them.
contact:
name: HealthLog
url: https://healthlog.bombeck.io
license:
name: MIT
url: https://github.com/MBombeck/healthlog/blob/main/LICENSE
servers:
- url: https://healthlog.bombeck.io
description: Production
- url: http://localhost:3000
description: Local development
tags:
- name: Auth
description: Registration, login (password + passkey/WebAuthn), logout, session info, profile, password change.
- name: Measurements
description: CRUD for measurements (weight, BP, pulse, body fat, glucose, sleep, steps).
- name: Medications
description: Medication management, schedules, intake events, compliance, reminder phase config.
- name: Mood
description: Mood entries (manual + moodLog imports) and mood analytics.
- name: Dashboard
description: User-customizable dashboard widget layout.
- name: Insights
description: AI-generated insights, classifications (BMI, BP, mood, etc.), per-metric status, target ranges.
- name: Achievements
description: Gamification achievements (streaks, milestones).
- name: DoctorReport
description: Aggregated health data for client-side PDF export.
- name: Integrations
description: Withings, moodLog, Codex (ChatGPT) OAuth integrations.
- name: User
description: User-level configuration (AI provider, threshold overrides).
- name: Tokens
description: User-issued API tokens for external ingest.
- name: Notifications
description: Notification channels and preferences (Telegram, ntfy, Web Push).
- name: Onboarding
description: First-run profile setup.
- name: Health
description: Service health probe.
- name: Settings
description: Per-user settings (account deletion, data wipe, integrations, notifications).
- name: Analytics
description: Aggregated analytics summaries.
- name: Export
description: User data export (CSV/JSON) and re-import.
- name: AuditLog
description: User-facing audit log of own activity.
- name: Feedback
description: User feedback submission and history.
- name: BugReport
description: User bug reports (creates a GitHub issue when configured).
- name: Monitoring
description: Public monitoring/error reporting endpoints (CSP, Glitchtip, Umami proxy).
- name: Webhooks
description: Inbound webhooks (Withings, moodLog, Telegram).
- name: Ingest
description: External ingest endpoints authenticated via Bearer token.
- name: Admin
description: Internal admin-only endpoints. Not consumed by the iOS client.
security:
- bearerAuth: []
- sessionCookie: []
paths:
# ─── Auth ────────────────────────────────────────────────────────
/api/auth/register:
post:
tags: [Auth]
summary: Register a new user (password)
operationId: registerUser
security: []
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/RegisterInput' }
responses:
'201':
description: Created
content:
application/json:
schema:
$ref: '#/components/schemas/RegisterResponseEnvelope'
'403': { $ref: '#/components/responses/Forbidden' }
'409': { $ref: '#/components/responses/Conflict' }
'422': { $ref: '#/components/responses/Unprocessable' }
'429': { $ref: '#/components/responses/RateLimited' }
'500': { $ref: '#/components/responses/ServerError' }
/api/auth/login:
post:
tags: [Auth]
summary: Login with email/username + password
operationId: loginUser
security: []
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/LoginInput' }
responses:
'200':
description: Authenticated
content:
application/json:
schema: { $ref: '#/components/schemas/LoginResponseEnvelope' }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
'429': { $ref: '#/components/responses/RateLimited' }
'500': { $ref: '#/components/responses/ServerError' }
/api/auth/logout:
post:
tags: [Auth]
summary: Destroy current session
operationId: logoutUser
responses:
'200':
description: Logged out
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
loggedOut: { type: boolean }
'500': { $ref: '#/components/responses/ServerError' }
/api/auth/me:
get:
tags: [Auth]
summary: Get current authenticated user
operationId: getCurrentUser
responses:
'200':
description: Current user
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/CurrentUser' }
'401': { $ref: '#/components/responses/Unauthorized' }
'500': { $ref: '#/components/responses/ServerError' }
/api/auth/profile:
put:
tags: [Auth]
summary: Update profile (email, height, DOB, gender)
operationId: updateProfile
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/ProfileInput' }
responses:
'200':
description: Updated profile
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/User' }
'401': { $ref: '#/components/responses/Unauthorized' }
'409': { $ref: '#/components/responses/Conflict' }
'422': { $ref: '#/components/responses/Unprocessable' }
'500': { $ref: '#/components/responses/ServerError' }
/api/auth/password:
post:
tags: [Auth]
summary: Change password (invalidates other sessions)
operationId: changePassword
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/ChangePasswordInput' }
responses:
'200':
description: Password changed
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
changed: { type: boolean }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
'429': { $ref: '#/components/responses/RateLimited' }
'500': { $ref: '#/components/responses/ServerError' }
/api/auth/registration-status:
get:
tags: [Auth]
summary: Whether public registration is enabled
operationId: getRegistrationStatus
security: []
responses:
'200':
description: Registration status
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
registrationEnabled: { type: boolean }
# ─── Passkeys (WebAuthn) ───────────────────────────────────────
/api/auth/passkey/register-options:
post:
tags: [Auth]
summary: Begin passkey registration — get WebAuthn options
operationId: getPasskeyRegisterOptions
responses:
'200':
description: WebAuthn registration options
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/PasskeyRegisterOptions' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/auth/passkey/register-verify:
post:
tags: [Auth]
summary: Complete passkey registration — verify attestation
operationId: verifyPasskeyRegistration
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/PasskeyVerifyRegistrationInput' }
responses:
'200':
description: Passkey registered
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
verified: { type: boolean }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
/api/auth/passkey/login-options:
post:
tags: [Auth]
summary: Begin passkey login — get WebAuthn options
operationId: getPasskeyLoginOptions
security: []
responses:
'200':
description: WebAuthn authentication options
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/PasskeyLoginOptions' }
'429': { $ref: '#/components/responses/RateLimited' }
/api/auth/passkey/login-verify:
post:
tags: [Auth]
summary: Complete passkey login — verify assertion
operationId: verifyPasskeyLogin
security: []
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/PasskeyVerifyLoginInput' }
responses:
'200':
description: Authenticated via passkey
content:
application/json:
schema: { $ref: '#/components/schemas/LoginResponseEnvelope' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/Unprocessable' }
'429': { $ref: '#/components/responses/RateLimited' }
/api/auth/passkeys:
get:
tags: [Auth]
summary: List the user's passkeys
operationId: listPasskeys
responses:
'200':
description: List of passkeys
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: array
items: { $ref: '#/components/schemas/PasskeySummary' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/auth/passkeys/{id}:
delete:
tags: [Auth]
summary: Delete a passkey
operationId: deletePasskey
parameters:
- $ref: '#/components/parameters/IdPathParam'
responses:
'200':
description: Deleted
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
success: { type: boolean }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
# ─── Codex (ChatGPT) OAuth ────────────────────────────────────
/api/auth/codex/authorize:
get:
tags: [Integrations]
summary: Begin ChatGPT OAuth flow (redirects to ChatGPT)
operationId: codexAuthorize
responses:
'302':
description: Redirect to ChatGPT auth URL
'401': { $ref: '#/components/responses/Unauthorized' }
'429': { $ref: '#/components/responses/RateLimited' }
/api/auth/codex/callback:
get:
tags: [Integrations]
summary: ChatGPT OAuth callback — exchange code for tokens
operationId: codexCallback
parameters:
- in: query
name: code
schema: { type: string }
- in: query
name: state
schema: { type: string }
- in: query
name: error
schema: { type: string }
responses:
'302':
description: Redirect back to /settings with status
'401': { $ref: '#/components/responses/Unauthorized' }
/api/auth/codex/disconnect:
delete:
tags: [Integrations]
summary: Disconnect ChatGPT integration
operationId: codexDisconnect
responses:
'200':
description: Disconnected
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
disconnected: { type: boolean }
'401': { $ref: '#/components/responses/Unauthorized' }
'429': { $ref: '#/components/responses/RateLimited' }
# ─── Measurements ────────────────────────────────────────────────
/api/measurements:
get:
tags: [Measurements]
summary: List measurements (paginated, filterable)
operationId: listMeasurements
parameters:
- in: query
name: type
schema: { $ref: '#/components/schemas/MeasurementType' }
- in: query
name: from
schema: { type: string, format: date-time }
- in: query
name: to
schema: { type: string, format: date-time }
- $ref: '#/components/parameters/LimitParam'
- $ref: '#/components/parameters/OffsetParam'
- in: query
name: sortBy
schema:
type: string
enum: [type, value, measuredAt, source]
default: measuredAt
- in: query
name: sortDir
schema:
type: string
enum: [asc, desc]
default: desc
responses:
'200':
description: List of measurements
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
measurements:
type: array
items: { $ref: '#/components/schemas/Measurement' }
meta: { $ref: '#/components/schemas/PageMeta' }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
'500': { $ref: '#/components/responses/ServerError' }
post:
tags: [Measurements]
summary: Create a measurement (single or batch)
operationId: createMeasurement
requestBody:
required: true
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/CreateMeasurementInput'
- type: array
items: { $ref: '#/components/schemas/CreateMeasurementInput' }
minItems: 1
maxItems: 5
responses:
'201':
description: Created
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
oneOf:
- $ref: '#/components/schemas/Measurement'
- type: array
items: { $ref: '#/components/schemas/Measurement' }
'401': { $ref: '#/components/responses/Unauthorized' }
'409': { $ref: '#/components/responses/Conflict' }
'422': { $ref: '#/components/responses/Unprocessable' }
'500': { $ref: '#/components/responses/ServerError' }
/api/measurements/{id}:
parameters:
- $ref: '#/components/parameters/IdPathParam'
get:
tags: [Measurements]
summary: Get a single measurement
operationId: getMeasurement
responses:
'200':
description: Measurement
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/Measurement' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
put:
tags: [Measurements]
summary: Update a measurement
operationId: updateMeasurement
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/UpdateMeasurementInput' }
responses:
'200':
description: Updated
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/Measurement' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/Unprocessable' }
delete:
tags: [Measurements]
summary: Delete a measurement
operationId: deleteMeasurement
responses:
'200':
description: Deleted
content:
application/json:
schema: { $ref: '#/components/schemas/DeletedEnvelope' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
# ─── Medications ─────────────────────────────────────────────────
/api/medications:
get:
tags: [Medications]
summary: List medications (with today's intake summary)
operationId: listMedications
responses:
'200':
description: List
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: array
items: { $ref: '#/components/schemas/MedicationWithSchedules' }
'401': { $ref: '#/components/responses/Unauthorized' }
post:
tags: [Medications]
summary: Create a medication with schedules
operationId: createMedication
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/CreateMedicationInput' }
responses:
'201':
description: Created
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/MedicationWithSchedules' }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
/api/medications/{id}:
parameters:
- $ref: '#/components/parameters/IdPathParam'
get:
tags: [Medications]
summary: Get a single medication
operationId: getMedication
responses:
'200':
description: Medication
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/MedicationWithSchedules' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
put:
tags: [Medications]
summary: Update a medication
operationId: updateMedication
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/UpdateMedicationInput' }
responses:
'200':
description: Updated
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/MedicationWithSchedules' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/Unprocessable' }
delete:
tags: [Medications]
summary: Delete a medication (revokes its API tokens)
operationId: deleteMedication
responses:
'200':
description: Deleted
content:
application/json:
schema: { $ref: '#/components/schemas/DeletedEnvelope' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/api/medications/{id}/intake:
parameters:
- $ref: '#/components/parameters/IdPathParam'
get:
tags: [Medications]
summary: List intake events for a medication
operationId: listMedicationIntakes
parameters:
- $ref: '#/components/parameters/LimitParam'
- $ref: '#/components/parameters/OffsetParam'
- in: query
name: sortBy
schema:
type: string
enum: [scheduledFor, takenAt, source, createdAt]
default: scheduledFor
- in: query
name: sortDir
schema: { type: string, enum: [asc, desc], default: desc }
responses:
'200':
description: Events
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
events:
type: array
items: { $ref: '#/components/schemas/MedicationIntakeEvent' }
meta: { $ref: '#/components/schemas/PageMeta' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/Unprocessable' }
post:
tags: [Medications]
summary: Log a medication intake or skip
operationId: createMedicationIntake
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/CreateIntakeInput' }
responses:
'200':
description: Idempotent return of existing event
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/MedicationIntakeEvent' }
'201':
description: Created
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/MedicationIntakeEvent' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/Unprocessable' }
/api/medications/{id}/intake/{eventId}:
parameters:
- $ref: '#/components/parameters/IdPathParam'
- in: path
name: eventId
required: true
schema: { type: string }
put:
tags: [Medications]
summary: Update an intake event
operationId: updateMedicationIntake
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/UpdateIntakeInput' }
responses:
'200':
description: Updated
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/MedicationIntakeEvent' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/Unprocessable' }
delete:
tags: [Medications]
summary: Delete an intake event
operationId: deleteMedicationIntake
responses:
'200':
description: Deleted
content:
application/json:
schema: { $ref: '#/components/schemas/DeletedEnvelope' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/api/medications/{id}/intake/import:
parameters:
- $ref: '#/components/parameters/IdPathParam'
post:
tags: [Medications]
summary: Bulk-import historical intakes for one medication
operationId: importMedicationIntakes
requestBody:
required: true
content:
application/json:
schema:
oneOf:
- type: array
items: { $ref: '#/components/schemas/IntakeImportEntry' }
minItems: 1
maxItems: 1000
- type: object
description: Object whose first array-valued property is treated as the entries.
additionalProperties: true
responses:
'201':
description: Import result
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/IntakeImportResult' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/Unprocessable' }
/api/medications/{id}/intake/purge:
parameters:
- $ref: '#/components/parameters/IdPathParam'
delete:
tags: [Medications]
summary: Delete ALL intake events for a medication
operationId: purgeMedicationIntakes
responses:
'200':
description: Purged
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
purged: { type: boolean }
count: { type: integer }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/api/medications/{id}/compliance:
parameters:
- $ref: '#/components/parameters/IdPathParam'
get:
tags: [Medications]
summary: Compliance summary for a medication (7d, 30d, 90 daily)
operationId: getMedicationCompliance
responses:
'200':
description: Compliance
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/MedicationComplianceResponse' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/api/medications/{id}/phase-config:
parameters:
- $ref: '#/components/parameters/IdPathParam'
get:
tags: [Medications]
summary: Get reminder-phase config (or defaults)
operationId: getPhaseConfig
responses:
'200':
description: Phase config
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/ReminderPhaseConfig' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
put:
tags: [Medications]
summary: Update reminder-phase config
operationId: updatePhaseConfig
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/ReminderPhaseConfig' }
responses:
'200':
description: Updated
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/ReminderPhaseConfig' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
tags: [Medications]
summary: Reset reminder-phase config to defaults
operationId: resetPhaseConfig
responses:
'200':
description: Reset
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
reset: { type: boolean }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/api/medications/{id}/api-endpoint:
parameters:
- $ref: '#/components/parameters/IdPathParam'
get:
tags: [Medications]
summary: Whether an external API token is enabled for this medication
operationId: getMedicationApiEndpoint
responses:
'200':
description: Status
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
enabled: { type: boolean }
activeTokenCount: { type: integer }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
put:
tags: [Medications]
summary: Enable / disable a medication-scoped API token
operationId: setMedicationApiEndpoint
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
enabled: { type: boolean }
required: [enabled]
responses:
'200':
description: Existing token reused or revocation result
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/MedicationApiEndpointResponse' }
'201':
description: New medication-scoped token created
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/MedicationApiEndpointResponse' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/Unprocessable' }
/api/medications/intake-summary:
get:
tags: [Medications]
summary: Daily intake summary (for streak chart)
operationId: getIntakeSummary
parameters:
- in: query
name: from
schema: { type: string, format: date-time }
- in: query
name: to
schema: { type: string, format: date-time }
responses:
'200':
description: Summary
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/IntakeSummaryResponse' }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
# ─── Mood ─────────────────────────────────────────────────────
/api/mood-entries:
get:
tags: [Mood]
summary: List mood entries (paginated, filterable)
operationId: listMoodEntries
parameters:
- in: query
name: mood
schema: { $ref: '#/components/schemas/MoodLevel' }
- in: query
name: from
description: 'YYYY-MM-DD lower bound (inclusive).'
schema: { type: string }
- in: query
name: to
description: 'YYYY-MM-DD upper bound (inclusive).'
schema: { type: string }
- $ref: '#/components/parameters/LimitParam'
- $ref: '#/components/parameters/OffsetParam'
- in: query
name: sortBy
schema: { type: string, default: moodLoggedAt }
- in: query
name: sortDir
schema: { type: string, enum: [asc, desc], default: desc }
responses:
'200':
description: Mood entries
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
entries:
type: array
items: { $ref: '#/components/schemas/MoodEntry' }
meta: { $ref: '#/components/schemas/PageMeta' }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
post:
tags: [Mood]
summary: Create a mood entry
operationId: createMoodEntry
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/CreateMoodEntryInput' }
responses:
'201':
description: Created
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/MoodEntry' }
'401': { $ref: '#/components/responses/Unauthorized' }
'409': { $ref: '#/components/responses/Conflict' }
'422': { $ref: '#/components/responses/Unprocessable' }
/api/mood-entries/{id}:
parameters:
- $ref: '#/components/parameters/IdPathParam'
get:
tags: [Mood]
summary: Get a mood entry
operationId: getMoodEntry
responses:
'200':
description: Mood entry
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/MoodEntry' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
put:
tags: [Mood]
summary: Update a mood entry
operationId: updateMoodEntry
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/UpdateMoodEntryInput' }
responses:
'200':
description: Updated
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/MoodEntry' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/Unprocessable' }
delete:
tags: [Mood]
summary: Delete a mood entry
operationId: deleteMoodEntry
responses:
'200':
description: Deleted
content:
application/json:
schema: { $ref: '#/components/schemas/DeletedEnvelope' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/api/mood/analytics:
get:
tags: [Mood]
summary: Mood analytics (daily averages + summary)
operationId: getMoodAnalytics
responses:
'200':
description: Analytics
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/MoodAnalytics' }
'401': { $ref: '#/components/responses/Unauthorized' }
# ─── Dashboard ────────────────────────────────────────────────
/api/dashboard/widgets:
get:
tags: [Dashboard]
summary: Get the resolved dashboard widget layout
operationId: getDashboardWidgets
responses:
'200':
description: Layout
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/DashboardLayout' }
'401': { $ref: '#/components/responses/Unauthorized' }
put:
tags: [Dashboard]
summary: Replace the dashboard widget layout
operationId: updateDashboardWidgets
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/DashboardLayout' }
responses:
'200':
description: Updated
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/DashboardLayout' }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
delete:
tags: [Dashboard]
summary: Reset dashboard layout to defaults
operationId: resetDashboardWidgets
responses:
'200':
description: Reset
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/DashboardLayout' }
'401': { $ref: '#/components/responses/Unauthorized' }
# ─── Insights ────────────────────────────────────────────────
/api/insights/comprehensive:
get:
tags: [Insights]
summary: Comprehensive 90-day analytics bundle
operationId: getComprehensiveInsights
responses:
'200':
description: Insights bundle
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/ComprehensiveInsights' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/insights/generate:
post:
tags: [Insights]
summary: Generate AI-narrative insights (cached for 24h)
operationId: generateInsights
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
force: { type: boolean, description: 'Bypass 24h cache' }
responses:
'200':
description: Insights
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/InsightGenerateResult' }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
'429': { $ref: '#/components/responses/RateLimited' }
'502':
description: Upstream AI provider failure
content:
application/json:
schema: { $ref: '#/components/schemas/ApiError' }
/api/insights/settings:
get:
tags: [Insights]
summary: Get insights settings (privacy mode, provider status)
operationId: getInsightsSettings
responses:
'200':
description: Settings
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/InsightsSettings' }
'401': { $ref: '#/components/responses/Unauthorized' }
put:
tags: [Insights]
summary: Update insights settings (privacy mode)
operationId: updateInsightsSettings
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
privacyMode: { type: string, enum: [aggregated, raw] }
responses:
'200':
description: Updated
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
updated: { type: boolean }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
/api/insights/targets:
get:
tags: [Insights]
summary: Per-metric target ranges + classification
operationId: getInsightTargets
responses:
'200':
description: Targets
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/InsightTargetsResponse' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/insights/blood-pressure-status:
get:
tags: [Insights]
summary: BP-specific narrative status
operationId: getBloodPressureStatus
parameters:
- $ref: '#/components/parameters/LocaleParam'
responses:
'200':
description: BP status
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/InsightStatusBundle' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/insights/bmi-status:
get:
tags: [Insights]
summary: BMI narrative status
operationId: getBmiStatus
parameters:
- $ref: '#/components/parameters/LocaleParam'
responses:
'200':
description: BMI status
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/InsightStatusBundle' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/insights/general-status:
get:
tags: [Insights]
summary: General health narrative status
operationId: getGeneralStatus
parameters:
- $ref: '#/components/parameters/LocaleParam'
responses:
'200':
description: Status
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/InsightStatusBundle' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/insights/medication-compliance-status:
get:
tags: [Insights]
summary: Medication compliance narrative status
operationId: getMedicationComplianceStatus
parameters:
- $ref: '#/components/parameters/LocaleParam'
responses:
'200':
description: Status
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/InsightStatusBundle' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/insights/mood-status:
get:
tags: [Insights]
summary: Mood narrative status
operationId: getMoodStatus
parameters:
- $ref: '#/components/parameters/LocaleParam'
responses:
'200':
description: Status
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/InsightStatusBundle' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/insights/pulse-status:
get:
tags: [Insights]
summary: Pulse narrative status
operationId: getPulseStatus
parameters:
- $ref: '#/components/parameters/LocaleParam'
responses:
'200':
description: Status
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/InsightStatusBundle' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/insights/weight-status:
get:
tags: [Insights]
summary: Weight narrative status
operationId: getWeightStatus
parameters:
- $ref: '#/components/parameters/LocaleParam'
responses:
'200':
description: Status
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/InsightStatusBundle' }
'401': { $ref: '#/components/responses/Unauthorized' }
# ─── Achievements ─────────────────────────────────────────────
/api/gamification/achievements:
get:
tags: [Achievements]
summary: Get achievements progress + unlocks
operationId: getAchievements
responses:
'200':
description: Achievements
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/AchievementsResult' }
'401': { $ref: '#/components/responses/Unauthorized' }
# ─── DoctorReport ─────────────────────────────────────────────
/api/doctor-report:
post:
tags: [DoctorReport]
summary: Aggregated health data for doctor-report PDF
operationId: getDoctorReport
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
days:
type: integer
minimum: 1
maximum: 365
default: 90
responses:
'200':
description: Report data
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/DoctorReportResponse' }
'401': { $ref: '#/components/responses/Unauthorized' }
'429': { $ref: '#/components/responses/RateLimited' }
# ─── Onboarding ───────────────────────────────────────────────
/api/onboarding/complete:
post:
tags: [Onboarding]
summary: Mark onboarding complete and save initial profile
operationId: completeOnboarding
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/OnboardingInput' }
responses:
'200':
description: Completed
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
completed: { type: boolean }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
# ─── Health probe ─────────────────────────────────────────────
/api/health:
get:
tags: [Health]
summary: Service liveness probe
operationId: healthCheck
security: []
responses:
'200':
description: Healthy
content:
application/json:
schema: { $ref: '#/components/schemas/HealthStatus' }
'503':
description: Degraded
content:
application/json:
schema: { $ref: '#/components/schemas/HealthStatus' }
# ─── Tokens ──────────────────────────────────────────────────
/api/tokens:
get:
tags: [Tokens]
summary: List the user's API tokens (metadata only — secrets never returned)
operationId: listTokens
responses:
'200':
description: Tokens
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: array
items: { $ref: '#/components/schemas/ApiTokenSummary' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
post:
tags: [Tokens]
summary: Create an API token (token returned ONCE)
operationId: createToken
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/CreateTokenInput' }
responses:
'201':
description: Created (token visible once)
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
token: { type: string, example: 'hlk_<hex>' }
name: { type: string }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'422': { $ref: '#/components/responses/Unprocessable' }
/api/tokens/{id}:
parameters:
- $ref: '#/components/parameters/IdPathParam'
delete:
tags: [Tokens]
summary: Revoke an API token
operationId: revokeToken
responses:
'200':
description: Revoked
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
revoked: { type: boolean }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
# ─── Ingest (external) ────────────────────────────────────────
/api/ingest/medication:
post:
tags: [Ingest]
summary: External medication intake ingest (Bearer token, idempotent)
operationId: ingestMedicationIntake
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/ExternalIntakeInput' }
responses:
'200':
description: Existing event returned (idempotent replay)
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/MedicationIntakeEvent' }
'201':
description: Created
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/MedicationIntakeEvent' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/Unprocessable' }
'429': { $ref: '#/components/responses/RateLimited' }
# ─── Notifications ────────────────────────────────────────────
/api/notifications/preferences:
get:
tags: [Notifications]
summary: List notification channels + preferences
operationId: getNotificationPreferences
responses:
'200':
description: Preferences
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/NotificationPreferencesResponse' }
'401': { $ref: '#/components/responses/Unauthorized' }
put:
tags: [Notifications]
summary: Toggle a single channel/event preference
operationId: updateNotificationPreference
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/UpdateNotificationPreferenceInput' }
responses:
'200':
description: Updated
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/NotificationPreference' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/Unprocessable' }
/api/notifications/vapid:
get:
tags: [Notifications]
summary: Get VAPID public key (Web Push)
operationId: getVapidKey
security: []
responses:
'200':
description: VAPID key
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
publicKey: { type: string }
'503':
description: Not configured
content:
application/json:
schema: { $ref: '#/components/schemas/ApiError' }
/api/notifications/web-push:
post:
tags: [Notifications]
summary: Subscribe a Web Push endpoint
operationId: subscribeWebPush
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/WebPushSubscribeInput' }
responses:
'200':
description: Subscribed
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
subscribed: { type: boolean }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
delete:
tags: [Notifications]
summary: Unsubscribe a Web Push endpoint
operationId: unsubscribeWebPush
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
endpoint: { type: string, format: uri }
required: [endpoint]
responses:
'200':
description: Unsubscribed
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
unsubscribed: { type: boolean }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
# ─── User configuration ───────────────────────────────────────
/api/user/ai-provider:
get:
tags: [User]
summary: Get user-level AI provider config
operationId: getUserAiProvider
responses:
'200':
description: Config
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/UserAiProvider' }
'401': { $ref: '#/components/responses/Unauthorized' }
patch:
tags: [User]
summary: Update user-level AI provider config
operationId: updateUserAiProvider
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/UpdateUserAiProviderInput' }
responses:
'200':
description: Updated
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
updated: { type: boolean }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
/api/user/thresholds:
get:
tags: [User]
summary: Get effective + override threshold ranges
operationId: getThresholds
responses:
'200':
description: Thresholds
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/ThresholdsResponse' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
put:
tags: [User]
summary: Set per-metric threshold overrides (partial)
operationId: updateThresholds
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/ThresholdOverrides' }
responses:
'200':
description: Updated
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
overrides: { $ref: '#/components/schemas/ThresholdOverrides' }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
delete:
tags: [User]
summary: Reset threshold overrides (all or one metric)
operationId: resetThresholds
parameters:
- in: query
name: metric
schema: { type: string }
description: 'If supplied, only this metric is reset; otherwise all overrides are cleared.'
responses:
'200':
description: Reset
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
overrides: { $ref: '#/components/schemas/ThresholdOverrides' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
# ─── Analytics + AI test ──────────────────────────────────────
/api/analytics:
get:
tags: [Analytics]
summary: Per-type measurement summaries + BMI + BP-in-target
operationId: getAnalytics
responses:
'200':
description: Analytics
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/AnalyticsResponse' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/ai/test:
post:
tags: [User]
summary: Test the configured AI provider
operationId: testAiProvider
responses:
'200':
description: AI test result
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/AiTestResult' }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
'429': { $ref: '#/components/responses/RateLimited' }
'502':
description: Upstream AI provider failure
content:
application/json:
schema: { $ref: '#/components/schemas/ApiError' }
# ─── Export / Import ──────────────────────────────────────────
/api/export:
get:
tags: [Export]
summary: Export user data (CSV or JSON)
operationId: exportData
parameters:
- in: query
name: format
schema: { type: string, enum: [csv, json], default: json }
- in: query
name: type
schema:
type: string
enum: [measurements, medications, intake, mood, all]
default: all
responses:
'200':
description: Export payload
content:
application/json:
schema:
type: object
properties:
data:
type: object
additionalProperties: true
text/csv:
schema: { type: string }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
'429': { $ref: '#/components/responses/RateLimited' }
/api/import:
post:
tags: [Export]
summary: Import previously exported data (JSON)
operationId: importData
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/ImportInput' }
responses:
'200':
description: Import stats
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/ImportResult' }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
# ─── Audit log + Feedback + Bug report ────────────────────────
/api/audit-log:
get:
tags: [AuditLog]
summary: Personal audit log entries
operationId: getAuditLog
parameters:
- $ref: '#/components/parameters/LimitParam'
- $ref: '#/components/parameters/OffsetParam'
responses:
'200':
description: Entries
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/AuditLogResponse' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/feedback:
get:
tags: [Feedback]
summary: List the user's submitted feedback
operationId: listFeedback
parameters:
- $ref: '#/components/parameters/LimitParam'
- $ref: '#/components/parameters/OffsetParam'
responses:
'200':
description: Feedback
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
items:
type: array
items: { $ref: '#/components/schemas/FeedbackSummary' }
meta: { $ref: '#/components/schemas/PageMeta' }
'401': { $ref: '#/components/responses/Unauthorized' }
post:
tags: [Feedback]
summary: Submit feedback (with optional screenshot)
operationId: submitFeedback
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/CreateFeedbackInput' }
responses:
'201':
description: Created
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/FeedbackSummary' }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
'429': { $ref: '#/components/responses/RateLimited' }
/api/bugreport:
post:
tags: [BugReport]
summary: File a bug report (creates a GitHub issue when configured)
operationId: submitBugReport
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/BugReportInput' }
responses:
'200':
description: Issue created
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
issueNumber: { type: integer }
issueUrl: { type: string, format: uri }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
'429': { $ref: '#/components/responses/RateLimited' }
'500': { $ref: '#/components/responses/ServerError' }
'503':
description: Bug report not configured
content:
application/json:
schema: { $ref: '#/components/schemas/ApiError' }
/api/bugreport/status:
get:
tags: [BugReport]
summary: Whether bug-report submission is configured
operationId: getBugReportStatus
responses:
'200':
description: Status
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
configured: { type: boolean }
isAdmin: { type: boolean }
'401': { $ref: '#/components/responses/Unauthorized' }
'429': { $ref: '#/components/responses/RateLimited' }
# ─── Settings (per-user) ──────────────────────────────────────
/api/settings/account:
delete:
tags: [Settings]
summary: Delete the user's account (and all data)
operationId: deleteAccount
responses:
'200':
description: Account deleted
content:
application/json:
schema: { $ref: '#/components/schemas/DeletedEnvelope' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/settings/data:
delete:
tags: [Settings]
summary: Wipe the user's data (keep account)
operationId: wipeData
responses:
'200':
description: Data wiped
content:
application/json:
schema: { $ref: '#/components/schemas/DeletedEnvelope' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/settings/global-services:
get:
tags: [Settings]
summary: Global service availability flags (telegram, ntfy, web-push, …)
operationId: getGlobalServices
responses:
'200':
description: Flags
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/GlobalServicesFlags' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/settings/moodlog:
put:
tags: [Settings]
summary: Configure moodLog integration
operationId: updateMoodLogSettings
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/MoodLogSettingsInput' }
responses:
'200':
description: Updated
content:
application/json:
schema: { $ref: '#/components/schemas/UpdatedEnvelope' }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
delete:
tags: [Settings]
summary: Disconnect moodLog integration
operationId: deleteMoodLogSettings
responses:
'200':
description: Removed
content:
application/json:
schema: { $ref: '#/components/schemas/DeletedEnvelope' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/settings/ntfy:
get:
tags: [Settings]
summary: Get ntfy channel settings
operationId: getNtfySettings
responses:
'200':
description: Config
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/NtfySettings' }
'401': { $ref: '#/components/responses/Unauthorized' }
put:
tags: [Settings]
summary: Update ntfy channel settings
operationId: updateNtfySettings
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/NtfySettingsInput' }
responses:
'200':
description: Updated
content:
application/json:
schema: { $ref: '#/components/schemas/UpdatedEnvelope' }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
/api/settings/ntfy/test:
post:
tags: [Settings]
summary: Send a test ntfy notification
operationId: testNtfy
responses:
'200':
description: Test result
content:
application/json:
schema: { $ref: '#/components/schemas/ApiEnvelope' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/settings/reminder-thresholds:
get:
tags: [Settings]
summary: Get reminder lateness thresholds (admin-defined defaults)
operationId: getReminderThresholds
responses:
'200':
description: Thresholds
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/ReminderThresholds' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/settings/telegram:
get:
tags: [Settings]
summary: Get Telegram channel settings
operationId: getTelegramSettings
responses:
'200':
description: Config
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/TelegramSettings' }
'401': { $ref: '#/components/responses/Unauthorized' }
put:
tags: [Settings]
summary: Update Telegram channel settings
operationId: updateTelegramSettings
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/TelegramSettingsInput' }
responses:
'200':
description: Updated
content:
application/json:
schema: { $ref: '#/components/schemas/UpdatedEnvelope' }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
/api/settings/telegram/test:
post:
tags: [Settings]
summary: Send a test Telegram message
operationId: testTelegram
responses:
'200':
description: Test result
content:
application/json:
schema: { $ref: '#/components/schemas/ApiEnvelope' }
'401': { $ref: '#/components/responses/Unauthorized' }
# ─── Withings integration ────────────────────────────────────
/api/withings/connect:
get:
tags: [Integrations]
summary: Begin Withings OAuth (302 redirect)
operationId: withingsConnect
responses:
'302':
description: Redirect to Withings
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/withings/callback:
get:
tags: [Integrations]
summary: Withings OAuth callback (302 to /settings)
operationId: withingsCallback
parameters:
- in: query
name: code
schema: { type: string }
- in: query
name: state
schema: { type: string }
responses:
'302':
description: Redirect back to /settings with status
/api/withings/disconnect:
post:
tags: [Integrations]
summary: Disconnect Withings
operationId: withingsDisconnect
responses:
'200':
description: Disconnected
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
disconnected: { type: boolean }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/api/withings/credentials:
get:
tags: [Integrations]
summary: Whether Withings client credentials are stored
operationId: getWithingsCredentialsStatus
responses:
'200':
description: Status
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
hasCredentials: { type: boolean }
'401': { $ref: '#/components/responses/Unauthorized' }
put:
tags: [Integrations]
summary: Save Withings client ID + secret (encrypted)
operationId: saveWithingsCredentials
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/WithingsCredentialsInput' }
responses:
'200':
description: Saved
content:
application/json:
schema: { $ref: '#/components/schemas/UpdatedEnvelope' }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
delete:
tags: [Integrations]
summary: Delete Withings credentials (and disconnect)
operationId: deleteWithingsCredentials
responses:
'200':
description: Deleted
content:
application/json:
schema: { $ref: '#/components/schemas/DeletedEnvelope' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/withings/status:
get:
tags: [Integrations]
summary: Withings connection status
operationId: withingsStatus
responses:
'200':
description: Status
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/WithingsStatus' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/withings/sync:
post:
tags: [Integrations]
summary: Trigger a Withings sync (incremental or full)
operationId: withingsSync
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
fullSync: { type: boolean, default: false }
responses:
'200':
description: Sync result
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
imported: { type: integer }
fullSync: { type: boolean }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/withings/webhook:
post:
tags: [Webhooks]
summary: Withings webhook (notification of new measurements)
operationId: withingsWebhook
security: []
parameters:
- in: query
name: secret
schema: { type: string }
description: 'Shared secret for webhook authentication.'
responses:
'200':
description: Acknowledged
content:
application/json:
schema:
type: object
properties:
status: { type: string }
'401':
description: Unauthorized
'429':
description: Rate limited
get:
tags: [Webhooks]
summary: Withings webhook URL verification
operationId: withingsWebhookVerifyGet
security: []
parameters:
- in: query
name: secret
schema: { type: string }
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
status: { type: string }
'401':
description: Unauthorized
head:
tags: [Webhooks]
summary: Withings webhook URL verification (HEAD)
operationId: withingsWebhookVerifyHead
security: []
parameters:
- in: query
name: secret
schema: { type: string }
responses:
'200':
description: OK
'401':
description: Unauthorized
# ─── moodLog integration ─────────────────────────────────────
/api/integrations/moodlog/status:
get:
tags: [Integrations]
summary: moodLog connection status
operationId: moodLogStatus
responses:
'200':
description: Status
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/MoodLogStatus' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/integrations/moodlog/sync:
post:
tags: [Integrations]
summary: Trigger a moodLog sync
operationId: moodLogSync
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
fullSync: { type: boolean, default: false }
responses:
'200':
description: Sync result
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
imported: { type: integer }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
/api/integrations/moodlog/webhook:
post:
tags: [Webhooks]
summary: Inbound webhook from moodLog
operationId: moodLogWebhook
security: []
parameters:
- in: header
name: X-Webhook-Secret
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: true
responses:
'200':
description: Acknowledged
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'429': { $ref: '#/components/responses/RateLimited' }
/api/telegram/webhook:
post:
tags: [Webhooks]
summary: Telegram inbound webhook
operationId: telegramWebhook
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: true
responses:
'200':
description: Acknowledged
get:
tags: [Webhooks]
summary: Telegram webhook health probe
operationId: telegramWebhookGet
security: []
responses:
'200':
description: OK
# ─── Public monitoring / proxies ─────────────────────────────
/api/monitoring/csp-report:
post:
tags: [Monitoring]
summary: Browser CSP violation report sink
operationId: cspReport
security: []
requestBody:
required: true
content:
application/csp-report:
schema:
type: object
additionalProperties: true
application/json:
schema:
type: object
additionalProperties: true
responses:
'204':
description: Accepted (no body)
/api/monitoring/glitchtip:
post:
tags: [Monitoring]
summary: Forward errors to Glitchtip
operationId: glitchtipReport
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: true
responses:
'204':
description: Accepted
/api/monitoring/settings:
get:
tags: [Monitoring]
summary: Public monitoring settings (umami / glitchtip flags)
operationId: getMonitoringSettings
security: []
responses:
'200':
description: Public settings
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/PublicMonitoringSettings' }
/api/monitoring/umami-script:
get:
tags: [Monitoring]
summary: Server-side proxy for the Umami tracker script
operationId: getUmamiScript
security: []
responses:
'200':
description: Tracker script
content:
application/javascript:
schema: { type: string }
/api/send:
post:
tags: [Monitoring]
summary: Server-side proxy for Umami /api/send
operationId: umamiProxySend
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: true
responses:
'200':
description: Forwarded
'204':
description: No-op when umami is disabled
'413':
description: Payload too large
# ─── Admin (internal) ────────────────────────────────────────
/api/admin/ai-settings:
get:
tags: [Admin]
security:
- sessionCookie: []
summary: 'Internal: get admin-managed AI fallback settings'
operationId: adminGetAiSettings
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
put:
tags: [Admin]
security:
- sessionCookie: []
summary: 'Internal: update admin-managed AI fallback settings'
operationId: adminUpdateAiSettings
requestBody:
required: true
content:
application/json:
schema: { type: object, additionalProperties: true }
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'422': { $ref: '#/components/responses/Unprocessable' }
/api/admin/audit-log:
get:
tags: [Admin]
security:
- sessionCookie: []
summary: 'Internal: cross-user audit log'
operationId: adminGetAuditLog
parameters:
- $ref: '#/components/parameters/LimitParam'
- $ref: '#/components/parameters/OffsetParam'
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
/api/admin/data:
delete:
tags: [Admin]
security:
- sessionCookie: []
summary: 'Internal: wipe per-user or all data'
operationId: adminWipeData
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
/api/admin/feedback:
get:
tags: [Admin]
security:
- sessionCookie: []
summary: 'Internal: list all feedback'
operationId: adminListFeedback
parameters:
- $ref: '#/components/parameters/LimitParam'
- $ref: '#/components/parameters/OffsetParam'
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
/api/admin/feedback/{id}:
parameters:
- $ref: '#/components/parameters/IdPathParam'
patch:
tags: [Admin]
security:
- sessionCookie: []
summary: 'Internal: update feedback status / admin note'
operationId: adminUpdateFeedback
requestBody:
required: true
content:
application/json:
schema: { type: object, additionalProperties: true }
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
tags: [Admin]
security:
- sessionCookie: []
summary: 'Internal: delete feedback'
operationId: adminDeleteFeedback
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
/api/admin/feedback/{id}/github:
parameters:
- $ref: '#/components/parameters/IdPathParam'
post:
tags: [Admin]
security:
- sessionCookie: []
summary: 'Internal: escalate feedback to a GitHub issue'
operationId: adminEscalateFeedback
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
/api/admin/monitoring/glitchtip-test:
post:
tags: [Admin]
security:
- sessionCookie: []
summary: 'Internal: send Glitchtip test event'
operationId: adminGlitchtipTest
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
/api/admin/monitoring/umami-test:
post:
tags: [Admin]
security:
- sessionCookie: []
summary: 'Internal: send Umami test ping'
operationId: adminUmamiTest
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
/api/admin/notifications/reminder-check:
post:
tags: [Admin]
security:
- sessionCookie: []
summary: 'Internal: trigger medication reminder evaluation'
operationId: adminTriggerReminderCheck
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
/api/admin/notifications/test:
post:
tags: [Admin]
security:
- sessionCookie: []
summary: 'Internal: send a test notification'
operationId: adminTestNotification
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
/api/admin/settings:
get:
tags: [Admin]
security:
- sessionCookie: []
summary: 'Internal: get app-wide settings'
operationId: adminGetSettings
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
put:
tags: [Admin]
security:
- sessionCookie: []
summary: 'Internal: update app-wide settings'
operationId: adminUpdateSettings
requestBody:
required: true
content:
application/json:
schema: { type: object, additionalProperties: true }
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'422': { $ref: '#/components/responses/Unprocessable' }
/api/admin/status:
get:
tags: [Admin]
security:
- sessionCookie: []
summary: 'Internal: deep system status'
operationId: adminStatus
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
/api/admin/tokens:
get:
tags: [Admin]
security:
- sessionCookie: []
summary: 'Internal: list all API tokens (cross-user)'
operationId: adminListTokens
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
/api/admin/users:
get:
tags: [Admin]
security:
- sessionCookie: []
summary: 'Internal: list all users'
operationId: adminListUsers
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
/api/admin/users/{id}:
parameters:
- $ref: '#/components/parameters/IdPathParam'
put:
tags: [Admin]
security:
- sessionCookie: []
summary: 'Internal: update a user (username/email/role)'
operationId: adminUpdateUser
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/AdminUpdateUserInput' }
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/Unprocessable' }
/api/admin/users/{id}/reset-password:
parameters:
- $ref: '#/components/parameters/IdPathParam'
post:
tags: [Admin]
security:
- sessionCookie: []
summary: "Internal: reset a user's password"
operationId: adminResetUserPassword
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/ApiEnvelope' } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
# ─── iOS adapter endpoints (v1.3) ────────────────────────────────────
/api/dashboard/summary:
get:
tags: [Dashboard]
summary: 'iOS: aggregated dashboard summary'
description: |
Single-call aggregate for the iOS DashboardSummary view. Combines
greeting, current/longest activity-day streak, today's medication
compliance, optional highlight insight, and per-metric latest +
7-day sparkline + trend.
operationId: getDashboardSummary
responses:
'200':
description: Dashboard summary
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/DashboardSummary' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/measurements/series:
get:
tags: [Measurements]
summary: 'iOS: time-series + summary stats for a metric kind'
operationId: getMeasurementSeries
parameters:
- in: query
name: kind
required: true
schema: { $ref: '#/components/schemas/MetricKind' }
- in: query
name: days
schema:
type: integer
minimum: 1
maximum: 365
default: 30
responses:
'200':
description: Series payload
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/MeasurementSeries' }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
/api/integrations/healthkit:
get:
tags: [Integrations]
summary: 'iOS: HealthKit per-metric direction config'
operationId: getHealthKitConfig
responses:
'200':
description: HealthKit config
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/HealthKitConfig' }
'401': { $ref: '#/components/responses/Unauthorized' }
patch:
tags: [Integrations]
summary: 'iOS: update HealthKit per-metric direction config'
operationId: updateHealthKitConfig
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/HealthKitConfigPatch' }
responses:
'200':
description: Updated config
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/HealthKitConfig' }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
/api/user/profile:
get:
tags: [Auth]
summary: 'iOS: flattened user profile'
operationId: getUserProfile
responses:
'200':
description: User profile
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/IosUserProfile' }
'401': { $ref: '#/components/responses/Unauthorized' }
patch:
tags: [Auth]
summary: 'iOS: update profile fields'
operationId: updateUserProfile
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/IosUserProfilePatch' }
responses:
'200':
description: Updated profile
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data: { $ref: '#/components/schemas/IosUserProfile' }
'401': { $ref: '#/components/responses/Unauthorized' }
'409': { $ref: '#/components/responses/Conflict' }
'422': { $ref: '#/components/responses/Unprocessable' }
/api/devices:
post:
tags: [Notifications]
summary: 'iOS: register an APNs device token'
operationId: registerDevice
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/DeviceRegisterInput' }
responses:
'201':
description: Device registered (or token transferred)
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
id: { type: string }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
/api/medications/intake:
get:
tags: [Medications]
summary: 'iOS: today list or compliance buckets'
operationId: listIntakeAggregator
parameters:
- in: query
name: scope
required: true
schema: { type: string, enum: [today, compliance] }
- in: query
name: days
schema:
type: integer
minimum: 1
maximum: 365
default: 30
responses:
'200':
description: Aggregator payload
content:
application/json:
schema: { $ref: '#/components/schemas/ApiEnvelope' }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/Unprocessable' }
post:
tags: [Medications]
summary: 'iOS: update an intake event by id (taken/skipped/snoozed)'
operationId: updateIntakeAggregator
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/IntakeUpdateInput' }
responses:
'200':
description: Updated intake event
content:
application/json:
schema: { $ref: '#/components/schemas/ApiEnvelope' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/Unprocessable' }
/api/insights/cards:
get:
tags: [Insights]
summary: 'iOS: rule-based insight cards'
operationId: getInsightCards
responses:
'200':
description: Insight cards
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: array
items: { $ref: '#/components/schemas/InsightCard' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/insights/correlations:
get:
tags: [Insights]
summary: 'iOS: cross-metric correlations (placeholder)'
operationId: getInsightCorrelations
description: |
Returns an empty array until the correlations engine is wired up
(Phase 7). Audited via `insights.correlations.empty`.
responses:
'200':
description: Correlations
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: array
items: { $ref: '#/components/schemas/InsightCorrelation' }
'401': { $ref: '#/components/responses/Unauthorized' }
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: 'hlk_<hex>'
description: |
Long-lived API token issued via `POST /api/tokens`. Accepted on all
user endpoints (interchangeable with the session cookie) and required
on external ingest endpoints (e.g. `/api/ingest/medication`). Tokens
are prefixed `hlk_` and stored as SHA-256 hashes server-side. Bearer
tokens never grant admin access — admin routes require the session
cookie. Permission scopes are enforced from the token's `permissions`
array.
sessionCookie:
type: apiKey
in: cookie
name: healthlog_session
description: |
HttpOnly session cookie set by `/api/auth/login` and the passkey
login flow. Authenticates the web app and (optionally) the iOS
client when used in a web view.
parameters:
IdPathParam:
in: path
name: id
required: true
schema:
type: string
description: 'CUID identifier.'
LimitParam:
in: query
name: limit
schema:
type: integer
minimum: 1
maximum: 500
default: 100
OffsetParam:
in: query
name: offset
schema:
type: integer
minimum: 0
default: 0
LocaleParam:
in: query
name: locale
schema:
type: string
enum: [de, en]
description: 'Override the resolved locale for narrative text.'
responses:
BadRequest:
description: Bad request
content:
application/json:
schema: { $ref: '#/components/schemas/ApiError' }
Unauthorized:
description: Authentication required
content:
application/json:
schema: { $ref: '#/components/schemas/ApiError' }
Forbidden:
description: Forbidden
content:
application/json:
schema: { $ref: '#/components/schemas/ApiError' }
NotFound:
description: Not found
content:
application/json:
schema: { $ref: '#/components/schemas/ApiError' }
Conflict:
description: Conflict (e.g. duplicate)
content:
application/json:
schema: { $ref: '#/components/schemas/ApiError' }
Unprocessable:
description: Validation failure
content:
application/json:
schema: { $ref: '#/components/schemas/ApiError' }
RateLimited:
description: Rate limit exceeded
content:
application/json:
schema: { $ref: '#/components/schemas/ApiError' }
ServerError:
description: Internal server error
content:
application/json:
schema: { $ref: '#/components/schemas/ApiError' }
schemas:
# ─── Envelope / errors ─────────────────────────────────────
ApiEnvelope:
type: object
description: Standard envelope. On success `error` is `null`; on failure `data` is `null` and `error` is a string.
properties:
data:
description: Response payload (typed per-endpoint).
error:
type: [string, 'null']
required: [data, error]
ApiError:
type: object
properties:
data:
type: 'null'
error:
type: string
required: [data, error]
DeletedEnvelope:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
deleted: { type: boolean }
UpdatedEnvelope:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
updated: { type: boolean }
PageMeta:
type: object
properties:
total: { type: integer }
limit: { type: integer }
offset: { type: integer }
required: [total, limit, offset]
# ─── Enums ─────────────────────────────────────────────────
MeasurementType:
type: string
enum:
- WEIGHT
- BLOOD_PRESSURE_SYS
- BLOOD_PRESSURE_DIA
- PULSE
- BODY_FAT
- SLEEP_DURATION
- ACTIVITY_STEPS
- BLOOD_GLUCOSE
MeasurementSource:
type: string
enum: [MANUAL, WITHINGS, IMPORT]
GlucoseContext:
type: string
enum: [FASTING, POSTPRANDIAL, RANDOM, BEDTIME]
IntakeSource:
type: string
enum: [WEB, API, REMINDER, IMPORT]
ReminderPhase:
type: string
enum: [GREEN, YELLOW, ORANGE, RED]
PhaseMode:
type: string
enum: [MINUTES, PERCENT]
UserRole:
type: string
enum: [ADMIN, USER]
Gender:
type: string
enum: [MALE, FEMALE]
MoodLevel:
type: string
enum: [SUPER_GUT, GUT, OKAY, SCHLECHT, LAUSIG]
MedicationCategory:
type: string
enum:
- BLOOD_PRESSURE
- VITAMIN
- SUPPLEMENT
- PAIN_RELIEF
- ALLERGY
- DIGESTIVE
- THYROID
- HORMONE
- SKIN
- SLEEP_AID
- OTHER
NotificationChannelType:
type: string
enum: [TELEGRAM, NTFY, WEB_PUSH]
AiProviderType:
type: string
enum: [OPENAI, ANTHROPIC, LOCAL, CHATGPT_OAUTH]
InsightSeverity:
type: string
enum: [normal, mild, moderate, severe, info]
Trend:
type: [string, 'null']
enum: [up, down, stable, null]
# ─── Auth DTOs ─────────────────────────────────────────────
RegisterInput:
type: object
properties:
email: { type: string, format: email }
username:
type: string
minLength: 3
maxLength: 30
pattern: '^[a-zA-Z0-9_-]+$'
password:
type: string
minLength: 12
required: [email, username, password]
LoginInput:
type: object
properties:
email:
type: string
description: Email or username.
password: { type: string }
required: [email, password]
LoginResponseEnvelope:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
user:
type: object
properties:
id: { type: string }
username: { type: string }
required: [id, username]
token:
type: string
description: |
Long-lived `hlk_*` Bearer token. Only returned when the
request carries `X-Client-Type: native` or a User-Agent
starting with `HealthLog-iOS`. Use it for subsequent
requests as `Authorization: Bearer <token>`.
tokenExpiresAt:
type: string
format: date-time
description: ISO timestamp at which the auto-issued token expires.
RegisterResponseEnvelope:
allOf:
- $ref: '#/components/schemas/ApiEnvelope'
- type: object
properties:
data:
type: object
properties:
user:
type: object
properties:
id: { type: string }
username: { type: string }
email: { type: [string, 'null'] }
ProfileInput:
type: object
properties:
email: { type: [string, 'null'], format: email }
heightCm:
type: [number, 'null']
minimum: 50
maximum: 300
dateOfBirth:
type: [string, 'null']
format: date
gender:
oneOf:
- $ref: '#/components/schemas/Gender'
- type: 'null'
ChangePasswordInput:
type: object
properties:
currentPassword: { type: string }
newPassword: { type: string, minLength: 12 }
confirmPassword: { type: string }
required: [currentPassword, newPassword, confirmPassword]
User:
type: object
description: Public-facing user shape.
properties:
id: { type: string }
username: { type: string }
email: { type: [string, 'null'], format: email }
role: { $ref: '#/components/schemas/UserRole' }
heightCm: { type: [number, 'null'] }
dateOfBirth: { type: [string, 'null'], format: date-time }
gender:
oneOf:
- $ref: '#/components/schemas/Gender'
- type: 'null'
timezone: { type: string }
required: [id, username, role, timezone]
CurrentUser:
allOf:
- $ref: '#/components/schemas/User'
- type: object
properties:
onboardingCompletedAt:
type: [string, 'null']
format: date-time
gravatarUrl:
type: [string, 'null']
glucoseUnit:
type: [string, 'null']
enum: ['mg/dL', 'mmol/L', null]
# ─── Passkey DTOs ──────────────────────────────────────────
PasskeyRegisterOptions:
type: object
description: SimpleWebAuthn `PublicKeyCredentialCreationOptionsJSON` plus a server-issued challenge id.
properties:
options:
type: object
additionalProperties: true
description: WebAuthn registration options.
challengeId: { type: string }
required: [options, challengeId]
PasskeyLoginOptions:
type: object
properties:
options:
type: object
additionalProperties: true
description: WebAuthn authentication options.
challengeId: { type: string }
required: [options, challengeId]
PasskeyVerifyRegistrationInput:
type: object
properties:
challengeId: { type: string }
credential:
type: object
additionalProperties: true
description: '`RegistrationResponseJSON` from `@simplewebauthn/browser`.'
required: [challengeId, credential]
PasskeyVerifyLoginInput:
type: object
properties:
challengeId: { type: string }
credential:
type: object
additionalProperties: true
description: '`AuthenticationResponseJSON` from `@simplewebauthn/browser`.'
required: [challengeId, credential]
PasskeySummary:
type: object
properties:
id: { type: string }
name: { type: string }
credentialDeviceType: { type: string }
credentialBackedUp: { type: boolean }
createdAt: { type: string, format: date-time }
required: [id, name, credentialDeviceType, credentialBackedUp, createdAt]
# ─── Measurement DTOs ──────────────────────────────────────
Measurement:
type: object
properties:
id: { type: string }
userId: { type: string }
type: { $ref: '#/components/schemas/MeasurementType' }
value: { type: number }
unit: { type: string }
source: { $ref: '#/components/schemas/MeasurementSource' }
measuredAt: { type: string, format: date-time }
notes: { type: [string, 'null'], maxLength: 25 }
externalId: { type: [string, 'null'] }
glucoseContext:
oneOf:
- $ref: '#/components/schemas/GlucoseContext'
- type: 'null'
createdAt: { type: string, format: date-time }
updatedAt: { type: string, format: date-time }
required: [id, userId, type, value, unit, source, measuredAt, createdAt, updatedAt]
CreateMeasurementInput:
type: object
properties:
type: { $ref: '#/components/schemas/MeasurementType' }
value: { type: number }
measuredAt: { type: string, format: date-time }
notes: { type: string, maxLength: 25 }
source:
allOf: [{ $ref: '#/components/schemas/MeasurementSource' }]
default: MANUAL
glucoseContext:
$ref: '#/components/schemas/GlucoseContext'
required: [type, value, measuredAt]
UpdateMeasurementInput:
type: object
properties:
value: { type: number, minimum: 0, maximum: 500000 }
measuredAt: { type: string, format: date-time }
notes: { type: [string, 'null'], maxLength: 25 }
# ─── Medication DTOs ───────────────────────────────────────
MedicationSchedule:
type: object
properties:
id: { type: string }
medicationId: { type: string }
windowStart:
type: string
pattern: '^([01]\d|2[0-3]):([0-5]\d)$'
description: HH:mm
windowEnd:
type: string
pattern: '^([01]\d|2[0-3]):([0-5]\d)$'
label: { type: [string, 'null'] }
dose: { type: [string, 'null'], description: 'Per-schedule dose override' }
daysOfWeek:
type: [string, 'null']
description: 'Serialized recurrence (CSV of 0..6, optional intervalWeeks suffix).'
required: [id, medicationId, windowStart, windowEnd]
Medication:
type: object
properties:
id: { type: string }
userId: { type: string }
name: { type: string }
dose: { type: string }
active: { type: boolean }
notificationsEnabled: { type: boolean }
pausedAt: { type: [string, 'null'], format: date-time }
snoozedUntil: { type: [string, 'null'], format: date-time }
createdAt: { type: string, format: date-time }
updatedAt: { type: string, format: date-time }
required: [id, userId, name, dose, active, notificationsEnabled, createdAt, updatedAt]
MedicationWithSchedules:
allOf:
- $ref: '#/components/schemas/Medication'
- type: object
properties:
schedules:
type: array
items: { $ref: '#/components/schemas/MedicationSchedule' }
category: { $ref: '#/components/schemas/MedicationCategory' }
lastTakenAt:
type: [string, 'null']
format: date-time
todayEventCount:
type: integer
MedicationScheduleInput:
type: object
properties:
windowStart: { type: string, pattern: '^([01]\d|2[0-3]):([0-5]\d)$' }
windowEnd: { type: string, pattern: '^([01]\d|2[0-3]):([0-5]\d)$' }
label: { type: string, maxLength: 50 }
dose: { type: string, maxLength: 50 }
daysOfWeek:
type: array
items: { type: integer, minimum: 0, maximum: 6 }
intervalWeeks:
type: integer
minimum: 1
maximum: 4
required: [windowStart, windowEnd]
CreateMedicationInput:
type: object
properties:
name: { type: string, minLength: 1, maxLength: 100 }
dose: { type: string, minLength: 1, maxLength: 50 }
category: { $ref: '#/components/schemas/MedicationCategory' }
schedules:
type: array
minItems: 1
items: { $ref: '#/components/schemas/MedicationScheduleInput' }
required: [name, dose, schedules]
UpdateMedicationInput:
type: object
properties:
name: { type: string, minLength: 1, maxLength: 100 }
dose: { type: string, minLength: 1, maxLength: 50 }
category: { $ref: '#/components/schemas/MedicationCategory' }
active: { type: boolean }
notificationsEnabled: { type: boolean }
schedules:
type: array
items: { $ref: '#/components/schemas/MedicationScheduleInput' }
MedicationIntakeEvent:
type: object
properties:
id: { type: string }
userId: { type: string }
medicationId: { type: string }
scheduledFor: { type: string, format: date-time }
takenAt: { type: [string, 'null'], format: date-time }
skipped: { type: boolean }
source: { $ref: '#/components/schemas/IntakeSource' }
idempotencyKey: { type: [string, 'null'] }
createdAt: { type: string, format: date-time }
required: [id, userId, medicationId, scheduledFor, skipped, source, createdAt]
CreateIntakeInput:
type: object
properties:
scheduledFor: { type: string, format: date-time }
takenAt: { type: string, format: date-time }
skipped: { type: boolean, default: false }
idempotencyKey: { type: string, maxLength: 128 }
UpdateIntakeInput:
type: object
properties:
takenAt: { type: [string, 'null'], format: date-time }
skipped: { type: boolean }
scheduledFor: { type: string, format: date-time }
IntakeImportEntry:
type: object
properties:
datum:
type: string
pattern: '^\d{4}-\d{2}-\d{2}$'
description: YYYY-MM-DD
uhrzeit:
type: string
pattern: '^\d{2}:\d{2}:\d{2}$'
description: HH:mm:ss
zaehler:
oneOf:
- { type: integer }
- { type: string }
required: [datum, uhrzeit]
IntakeImportResult:
type: object
properties:
imported: { type: integer }
skippedDuplicates: { type: integer }
skippedInvalid: { type: integer }
ExternalIntakeInput:
type: object
properties:
medicationName: { type: string, maxLength: 200 }
takenAt: { type: string, format: date-time }
idempotencyKey: { type: string, maxLength: 128 }
required: [medicationName, idempotencyKey]
DailyComplianceEntry:
type: object
properties:
expected: { type: integer }
taken: { type: integer }
skipped: { type: integer }
onTime: { type: integer }
late: { type: integer }
veryLate: { type: integer }
ComplianceSummary:
type: object
properties:
rate: { type: number, description: 'Fraction 0..1' }
taken: { type: integer }
skipped: { type: integer }
missed: { type: integer }
streak: { type: integer }
MedicationComplianceResponse:
type: object
properties:
compliance7: { $ref: '#/components/schemas/ComplianceSummary' }
compliance30: { $ref: '#/components/schemas/ComplianceSummary' }
dailyCompliance:
type: object
additionalProperties: { $ref: '#/components/schemas/DailyComplianceEntry' }
ReminderPhaseConfig:
type: object
properties:
greenValue: { type: integer, default: 60 }
greenMode: { allOf: [{ $ref: '#/components/schemas/PhaseMode' }], default: MINUTES }
yellowValue: { type: integer, default: 30 }
yellowMode: { allOf: [{ $ref: '#/components/schemas/PhaseMode' }], default: MINUTES }
orangeValue: { type: integer, default: 0 }
orangeMode: { allOf: [{ $ref: '#/components/schemas/PhaseMode' }], default: MINUTES }
redValue: { type: integer, default: 240 }
redMode: { allOf: [{ $ref: '#/components/schemas/PhaseMode' }], default: MINUTES }
MedicationApiEndpointResponse:
type: object
properties:
enabled: { type: boolean }
activeTokenCount: { type: integer }
token:
type: [string, 'null']
description: 'Plaintext token, only returned once on creation.'
created: { type: boolean }
revokedTokenCount: { type: integer }
IntakeSummaryPoint:
type: object
properties:
date:
type: string
description: YYYY-MM-DD (Berlin local).
total: { type: integer }
medications:
type: object
additionalProperties: { type: integer }
required: [date, total]
IntakeSummaryResponse:
type: object
properties:
points:
type: array
items: { $ref: '#/components/schemas/IntakeSummaryPoint' }
medications:
type: array
items: { type: string }
# ─── Mood DTOs ─────────────────────────────────────────────
MoodEntry:
type: object
properties:
id: { type: string }
userId: { type: string }
date:
type: string
description: YYYY-MM-DD
mood: { $ref: '#/components/schemas/MoodLevel' }
score:
type: integer
minimum: 1
maximum: 5
tags:
type: array
items: { type: string }
source: { type: string, default: MANUAL }
moodLoggedAt: { type: string, format: date-time }
syncedAt: { type: string, format: date-time }
createdAt: { type: string, format: date-time }
updatedAt: { type: string, format: date-time }
required: [id, userId, date, mood, score, source, moodLoggedAt]
CreateMoodEntryInput:
type: object
properties:
mood: { $ref: '#/components/schemas/MoodLevel' }
moodLoggedAt: { type: string, format: date-time }
tags:
type: array
items: { type: string }
source: { type: string }
required: [mood, moodLoggedAt]
UpdateMoodEntryInput:
type: object
properties:
mood: { $ref: '#/components/schemas/MoodLevel' }
moodLoggedAt: { type: string, format: date-time }
tags:
type: array
items: { type: string }
MoodAnalytics:
type: object
properties:
entries:
type: array
items:
type: object
properties:
date: { type: string }
score: { type: number }
samples: { type: integer }
summary:
type: object
additionalProperties: true
# ─── Dashboard DTOs ────────────────────────────────────────
DashboardWidget:
type: object
properties:
id:
type: string
enum: [weight, bp, pulse, bodyFat, mood, medications, sleep, steps, glucose, bpInTarget]
visible: { type: boolean }
order: { type: integer, minimum: 0, maximum: 99 }
required: [id, visible, order]
DashboardLayout:
type: object
properties:
version: { type: integer, enum: [1] }
widgets:
type: array
minItems: 1
maxItems: 20
items: { $ref: '#/components/schemas/DashboardWidget' }
required: [version, widgets]
# ─── Insights DTOs ─────────────────────────────────────────
Classification:
type: object
properties:
category: { type: string }
color: { type: string }
severity: { $ref: '#/components/schemas/InsightSeverity' }
SummaryStats:
type: object
properties:
latest: { type: [number, 'null'] }
avg7: { type: [number, 'null'] }
avg30: { type: [number, 'null'] }
min: { type: [number, 'null'] }
max: { type: [number, 'null'] }
count: { type: integer }
slope30:
type: [object, 'null']
properties:
slope: { type: number }
r2: { type: number }
anomalyCount: { type: integer }
CorrelationResult:
type: [object, 'null']
properties:
r: { type: number }
strength: { type: string }
n: { type: integer }
Alert:
type: object
properties:
severity: { $ref: '#/components/schemas/InsightSeverity' }
message: { type: string }
type: { type: string }
BpTargets:
type: [object, 'null']
properties:
sysLow: { type: integer }
sysHigh: { type: integer }
diaLow: { type: integer }
diaHigh: { type: integer }
ComprehensiveInsights:
type: object
properties:
summaries:
type: object
additionalProperties: { $ref: '#/components/schemas/SummaryStats' }
bmi: { type: [number, 'null'] }
bmiClassification:
oneOf:
- $ref: '#/components/schemas/Classification'
- type: 'null'
bpClassification:
oneOf:
- $ref: '#/components/schemas/Classification'
- type: 'null'
bpPctInTarget: { type: [integer, 'null'] }
bpTargets: { $ref: '#/components/schemas/BpTargets' }
weightBpCorrelation: { $ref: '#/components/schemas/CorrelationResult' }
scatterData:
type: array
items:
type: object
properties:
weight: { type: number }
sysBP: { type: number }
bpMedicationCorrelation:
type: [object, 'null']
properties:
r: { type: number }
strength: { type: string }
n: { type: integer }
medicationCount: { type: integer }
bpMedicationScatterData:
type: array
items:
type: object
properties:
continuityPct: { type: integer }
sysBP: { type: number }
moodSummary:
oneOf:
- $ref: '#/components/schemas/SummaryStats'
- type: 'null'
moodBpCorrelation: { $ref: '#/components/schemas/CorrelationResult' }
moodBpScatterData:
type: array
items:
type: object
properties:
mood: { type: number }
sysBP: { type: number }
moodWeightCorrelation: { $ref: '#/components/schemas/CorrelationResult' }
moodWeightScatterData:
type: array
items:
type: object
properties:
mood: { type: number }
weight: { type: number }
moodPulseCorrelation: { $ref: '#/components/schemas/CorrelationResult' }
moodPulseScatterData:
type: array
items:
type: object
properties:
mood: { type: number }
pulse: { type: number }
medications:
type: array
items:
type: object
properties:
id: { type: string }
name: { type: string }
dose: { type: string }
category: { $ref: '#/components/schemas/MedicationCategory' }
compliance7: { type: number }
compliance30: { type: number }
streak: { type: integer }
taken7: { type: integer }
skipped7: { type: integer }
missed7: { type: integer }
alerts:
type: array
items: { $ref: '#/components/schemas/Alert' }
hasProvider: { type: boolean }
dataSpanDays: { type: integer }
totalMeasurements: { type: integer }
InsightTargetItem:
type: object
properties:
type: { type: string }
label: { type: string }
current: { type: [number, 'null'] }
average30: { type: [number, 'null'] }
trend: { $ref: '#/components/schemas/Trend' }
unit: { type: string }
range:
type: [object, 'null']
properties:
min: { type: number }
max: { type: number }
classification:
oneOf:
- $ref: '#/components/schemas/Classification'
- type: 'null'
source: { type: string }
details:
type: object
properties:
medications:
type: array
items:
type: object
properties:
name: { type: string }
compliance7: { type: number }
compliance30: { type: number }
InsightTargetsResponse:
type: object
properties:
targets:
type: array
items: { $ref: '#/components/schemas/InsightTargetItem' }
bpDiastolic:
type: object
properties:
current: { type: [number, 'null'] }
average30: { type: [number, 'null'] }
range:
type: [object, 'null']
properties:
min: { type: number }
max: { type: number }
profile:
type: object
properties:
heightCm: { type: [number, 'null'] }
age: { type: [integer, 'null'] }
gender:
oneOf:
- $ref: '#/components/schemas/Gender'
- type: 'null'
glucoseUnit: { type: string }
InsightStatusBundle:
type: object
description: Narrative status payload (shape varies per status route).
additionalProperties: true
InsightGenerateResult:
type: object
properties:
insights:
type: object
additionalProperties: true
cached: { type: boolean }
cachedAt: { type: string, format: date-time }
InsightsSettings:
type: object
properties:
codexStatus: { type: string, enum: [connected, disconnected] }
codexConnectedAt: { type: [string, 'null'], format: date-time }
hasAdminKey: { type: boolean }
privacyMode: { type: string, enum: [aggregated, raw] }
lastInsightAt: { type: [string, 'null'], format: date-time }
# ─── Achievements ──────────────────────────────────────────
Achievement:
type: object
properties:
id: { type: string }
title: { type: string }
description: { type: string }
target: { type: integer }
progress: { type: integer }
unlocked: { type: boolean }
completedAt:
type: [string, 'null']
format: date-time
AchievementsResult:
type: object
properties:
achievements:
type: array
items: { $ref: '#/components/schemas/Achievement' }
unlockedCount: { type: integer }
totalCount: { type: integer }
# ─── Doctor report ─────────────────────────────────────────
DoctorReportResponse:
type: object
properties:
period:
type: object
properties:
days: { type: integer }
since: { type: string, format: date-time }
patient:
type: object
properties:
username: { type: [string, 'null'] }
dateOfBirth: { type: [string, 'null'], format: date-time }
gender:
oneOf:
- $ref: '#/components/schemas/Gender'
- type: 'null'
heightCm: { type: [number, 'null'] }
measurements:
type: object
additionalProperties:
type: array
items:
type: object
properties:
value: { type: number }
measuredAt: { type: string, format: date-time }
stats:
type: object
additionalProperties:
type: object
properties:
avg: { type: number }
min: { type: number }
max: { type: number }
count: { type: integer }
latest: { type: number }
glucoseStats:
type: object
additionalProperties:
type: object
properties:
avg: { type: number }
min: { type: number }
max: { type: number }
count: { type: integer }
latest: { type: number }
glucoseRanges:
type: object
additionalProperties:
type: object
properties:
min: { type: number }
max: { type: number }
glucoseUnit: { type: string }
bmi: { type: [number, 'null'] }
compliance:
type: object
additionalProperties:
type: object
properties:
total: { type: integer }
taken: { type: integer }
skipped: { type: integer }
missed: { type: integer }
medications:
type: array
items:
type: object
properties:
name: { type: string }
dose: { type: string }
schedules:
type: array
items:
type: object
properties:
windowStart: { type: string }
windowEnd: { type: string }
label: { type: [string, 'null'] }
mood:
type: [object, 'null']
properties:
avg: { type: number }
min: { type: number }
max: { type: number }
count: { type: integer }
distribution:
type: object
additionalProperties: { type: integer }
# ─── Onboarding ────────────────────────────────────────────
OnboardingInput:
type: object
properties:
heightCm: { type: number, minimum: 50, maximum: 300 }
dateOfBirth: { type: string, format: date }
gender: { $ref: '#/components/schemas/Gender' }
# ─── Health probe ──────────────────────────────────────────
HealthStatus:
type: object
properties:
status: { type: string, enum: [ok, degraded] }
timestamp: { type: string, format: date-time }
database: { type: string }
worker: { type: string }
workerLastHeartbeat: { type: string, format: date-time }
# ─── Tokens ────────────────────────────────────────────────
ApiTokenSummary:
type: object
properties:
id: { type: string }
name: { type: string }
permissions:
type: array
items: { type: string }
lastUsedAt: { type: [string, 'null'], format: date-time }
expiresAt: { type: [string, 'null'], format: date-time }
createdAt: { type: string, format: date-time }
revoked: { type: boolean }
CreateTokenInput:
type: object
properties:
name: { type: string, minLength: 1, maxLength: 100 }
expiresInDays: { type: integer, minimum: 1, maximum: 365 }
required: [name]
# ─── Notifications ─────────────────────────────────────────
NotificationChannel:
type: object
properties:
id: { type: string }
type: { $ref: '#/components/schemas/NotificationChannelType' }
label: { type: string }
enabled: { type: boolean }
globallyEnabled: { type: boolean }
NotificationPreference:
type: object
properties:
channelId: { type: string }
eventType: { type: string }
enabled: { type: boolean }
NotificationPreferencesResponse:
type: object
properties:
channels:
type: array
items: { $ref: '#/components/schemas/NotificationChannel' }
preferences:
type: array
items: { $ref: '#/components/schemas/NotificationPreference' }
eventTypes:
type: array
items: { type: string }
UpdateNotificationPreferenceInput:
type: object
properties:
channelId: { type: string }
eventType: { type: string }
enabled: { type: boolean }
required: [channelId, eventType, enabled]
WebPushSubscribeInput:
type: object
properties:
endpoint: { type: string, format: uri }
keys:
type: object
properties:
p256dh: { type: string }
auth: { type: string }
required: [p256dh, auth]
required: [endpoint, keys]
# ─── User config ───────────────────────────────────────────
UserAiProvider:
type: object
properties:
provider:
oneOf:
- $ref: '#/components/schemas/AiProviderType'
- type: 'null'
model: { type: [string, 'null'] }
baseUrl: { type: [string, 'null'] }
hasAnthropicKey: { type: boolean }
anthropicKeyPreview: { type: [string, 'null'] }
hasLocalKey: { type: boolean }
UpdateUserAiProviderInput:
type: object
properties:
provider:
oneOf:
- $ref: '#/components/schemas/AiProviderType'
- type: 'null'
model: { type: [string, 'null'] }
baseUrl: { type: [string, 'null'] }
anthropicKey: { type: [string, 'null'] }
localKey: { type: [string, 'null'] }
ThresholdRange:
type: object
properties:
greenMin: { type: number }
greenMax: { type: number }
orangeMin: { type: number }
orangeMax: { type: number }
redMin: { type: number }
redMax: { type: number }
EffectiveRange:
type: object
properties:
range:
oneOf:
- $ref: '#/components/schemas/ThresholdRange'
- type: 'null'
isOverride: { type: boolean }
defaultRange:
oneOf:
- $ref: '#/components/schemas/ThresholdRange'
- type: 'null'
ThresholdOverrides:
type: object
description: 'Map of metric name → `{ min, max }` override.'
additionalProperties:
type: object
properties:
min: { type: number }
max: { type: number }
ThresholdsResponse:
type: object
properties:
effective:
type: object
additionalProperties: { $ref: '#/components/schemas/EffectiveRange' }
overrides: { $ref: '#/components/schemas/ThresholdOverrides' }
# ─── Analytics ─────────────────────────────────────────────
AnalyticsResponse:
type: object
properties:
summaries:
type: object
additionalProperties: { $ref: '#/components/schemas/SummaryStats' }
bmi: { type: [number, 'null'] }
bpInTargetPct: { type: [integer, 'null'] }
glucoseByContext:
type: object
additionalProperties: { $ref: '#/components/schemas/SummaryStats' }
AiTestResult:
type: object
properties:
ok: { type: boolean }
providerType: { $ref: '#/components/schemas/AiProviderType' }
model: { type: string }
tokensUsed:
type: object
properties:
input: { type: integer }
output: { type: integer }
sample: { type: string }
# ─── Export / Import ───────────────────────────────────────
ImportInput:
type: object
properties:
measurements:
type: array
maxItems: 10000
items:
type: object
properties:
type: { $ref: '#/components/schemas/MeasurementType' }
value: { type: number }
unit: { type: string }
measuredAt: { type: string, format: date-time }
source: { type: string }
notes: { type: string }
required: [type, value, unit, measuredAt]
moodEntries:
type: array
maxItems: 10000
items:
type: object
properties:
date: { type: string }
mood: { $ref: '#/components/schemas/MoodLevel' }
score: { type: integer, minimum: 1, maximum: 5 }
tags: { type: string }
loggedAt: { type: string, format: date-time }
required: [date, mood, score]
ImportResult:
type: object
properties:
measurements: { type: integer }
moodEntries: { type: integer }
skipped: { type: integer }
# ─── Audit log ─────────────────────────────────────────────
AuditLogEntry:
type: object
properties:
id: { type: string }
action: { type: string }
ipAddress: { type: [string, 'null'] }
location: { type: [string, 'null'] }
details: { type: [string, 'null'], description: 'JSON string' }
createdAt: { type: string, format: date-time }
AuditLogResponse:
type: object
properties:
entries:
type: array
items: { $ref: '#/components/schemas/AuditLogEntry' }
meta: { $ref: '#/components/schemas/PageMeta' }
# ─── Feedback ──────────────────────────────────────────────
FeedbackCategory:
type: string
enum: [BUG, FEATURE_REQUEST, QUESTION, OTHER]
FeedbackStatus:
type: string
enum: [OPEN, ACKNOWLEDGED, RESOLVED, ARCHIVED]
FeedbackSummary:
type: object
properties:
id: { type: string }
category: { $ref: '#/components/schemas/FeedbackCategory' }
subject: { type: string }
status: { $ref: '#/components/schemas/FeedbackStatus' }
adminNote: { type: [string, 'null'] }
gitHubIssueUrl: { type: [string, 'null'], format: uri }
createdAt: { type: string, format: date-time }
updatedAt: { type: string, format: date-time }
CreateFeedbackInput:
type: object
properties:
category: { $ref: '#/components/schemas/FeedbackCategory' }
subject: { type: string }
description: { type: string }
screenshot:
type: string
description: 'Base64-encoded PNG/JPG (≤5 MB).'
metadata:
type: object
additionalProperties: true
required: [category, subject, description]
BugReportInput:
type: object
properties:
description: { type: string, minLength: 10, maxLength: 5000 }
screenshot:
type: string
description: '`data:image/(png|jpeg|webp|gif);base64,...` data URL.'
required: [description]
# ─── Settings ──────────────────────────────────────────────
GlobalServicesFlags:
type: object
properties:
telegram: { type: boolean }
ntfy: { type: boolean }
webPush: { type: boolean }
api: { type: boolean }
moodLog: { type: boolean }
MoodLogSettingsInput:
type: object
properties:
url: { type: [string, 'null'], format: uri }
apiKey: { type: [string, 'null'] }
enabled: { type: boolean }
webhookSecret: { type: [string, 'null'] }
NtfySettings:
type: object
properties:
configured: { type: boolean }
enabled: { type: boolean }
topic: { type: [string, 'null'] }
serverUrl: { type: [string, 'null'] }
NtfySettingsInput:
type: object
properties:
topic: { type: [string, 'null'] }
serverUrl: { type: [string, 'null'], format: uri }
enabled: { type: boolean }
accessToken: { type: [string, 'null'] }
TelegramSettings:
type: object
properties:
configured: { type: boolean }
enabled: { type: boolean }
chatId: { type: [string, 'null'] }
TelegramSettingsInput:
type: object
properties:
botToken: { type: [string, 'null'] }
chatId: { type: [string, 'null'] }
enabled: { type: boolean }
ReminderThresholds:
type: object
properties:
reminderLateMinutes: { type: integer }
reminderMissedMinutes: { type: integer }
# ─── Withings + moodLog status ─────────────────────────────
WithingsCredentialsInput:
type: object
properties:
clientId: { type: string }
clientSecret: { type: string }
required: [clientId, clientSecret]
WithingsStatus:
type: object
properties:
connected: { type: boolean }
configured: { type: boolean }
lastSyncedAt: { type: [string, 'null'], format: date-time }
connectedAt: { type: string, format: date-time }
tokenExpired: { type: boolean }
tokenRefreshFailed: { type: boolean }
tokenExpiresAt: { type: string, format: date-time }
MoodLogStatus:
type: object
properties:
configured: { type: boolean }
enabled: { type: boolean }
lastSyncedAt: { type: [string, 'null'], format: date-time }
entryCount: { type: integer }
webhookSecret: { type: [string, 'null'] }
PublicMonitoringSettings:
type: object
properties:
umamiEnabled: { type: boolean }
umamiScriptUrl: { type: [string, 'null'] }
umamiWebsiteId: { type: [string, 'null'] }
glitchtipEnabled: { type: boolean }
glitchtipDsn: { type: [string, 'null'] }
glitchtipEnvironment: { type: [string, 'null'] }
# ─── iOS adapter DTOs (v1.3) ──────────────────────────────
MetricKind:
type: string
enum:
- weight
- bloodPressure
- pulse
- bodyFat
- glucose
- sleep
- steps
InsightSeverityIos:
type: string
enum: [alert, caution, info, good]
DashboardSummaryGreeting:
type: object
properties:
salutation: { type: string }
date: { type: string, format: date-time }
required: [salutation, date]
DashboardSummaryStreak:
type: object
properties:
currentDays: { type: integer }
longest: { type: integer }
label: { type: string }
required: [currentDays, longest, label]
DashboardSummaryCompliance:
type: object
properties:
scheduledToday: { type: integer }
takenToday: { type: integer }
required: [scheduledToday, takenToday]
DashboardMetricCard:
type: object
properties:
id: { type: string }
kind: { $ref: '#/components/schemas/MetricKind' }
title: { type: string }
latestValue: { type: [number, 'null'] }
secondaryValue: { type: [number, 'null'] }
unit: { type: string }
trend:
type: string
enum: [up, down, flat, unknown]
sparkline:
type: array
items: { type: number }
updatedAt: { type: [string, 'null'], format: date-time }
required: [id, kind, title, unit, trend, sparkline]
DashboardSummary:
type: object
properties:
greeting: { $ref: '#/components/schemas/DashboardSummaryGreeting' }
streak: { $ref: '#/components/schemas/DashboardSummaryStreak' }
compliance: { $ref: '#/components/schemas/DashboardSummaryCompliance' }
highlightInsight:
oneOf:
- { type: 'null' }
- { $ref: '#/components/schemas/InsightCard' }
metrics:
type: array
items: { $ref: '#/components/schemas/DashboardMetricCard' }
lastUpdated: { type: string, format: date-time }
required: [greeting, streak, compliance, metrics, lastUpdated]
MeasurementSeriesPoint:
type: object
properties:
id: { type: string }
at: { type: string, format: date-time }
value: { type: number }
secondary: { type: [number, 'null'] }
required: [id, at, value]
MeasurementSeriesStats:
type: object
properties:
mean: { type: number }
min: { type: number }
max: { type: number }
stdDev: { type: number }
count: { type: integer }
required: [mean, min, max, stdDev, count]
MeasurementSeries:
type: object
properties:
kind: { $ref: '#/components/schemas/MetricKind' }
points:
type: array
items: { $ref: '#/components/schemas/MeasurementSeriesPoint' }
stats: { $ref: '#/components/schemas/MeasurementSeriesStats' }
required: [kind, points, stats]
HealthKitDirection:
type: string
enum: [bidirectional, readOnly, writeOnly, disabled]
HealthKitEntry:
type: object
properties:
id: { type: string }
kind: { type: string }
direction: { $ref: '#/components/schemas/HealthKitDirection' }
enabled: { type: boolean }
required: [id, kind, direction, enabled]
HealthKitConfig:
type: object
properties:
entries:
type: array
items: { $ref: '#/components/schemas/HealthKitEntry' }
lastSyncedAt: { type: [string, 'null'], format: date-time }
required: [entries]
HealthKitConfigPatch:
type: object
properties:
entries:
type: array
maxItems: 50
items:
type: object
properties:
id: { type: string }
kind: { type: string }
direction: { $ref: '#/components/schemas/HealthKitDirection' }
enabled: { type: boolean }
required: [id, direction]
required: [entries]
IosUserProfile:
type: object
properties:
username: { type: string }
displayName: { type: [string, 'null'] }
email: { type: [string, 'null'], format: email }
dateOfBirth: { type: [string, 'null'], format: date-time }
gender: { $ref: '#/components/schemas/Gender' }
heightCm: { type: [number, 'null'] }
locale: { type: [string, 'null'] }
timezone: { type: string }
required: [username, timezone]
IosUserProfilePatch:
type: object
properties:
displayName: { type: [string, 'null'], minLength: 1, maxLength: 80 }
email: { type: [string, 'null'], format: email }
dateOfBirth: { type: [string, 'null'] }
gender:
oneOf:
- { type: 'null' }
- { $ref: '#/components/schemas/Gender' }
heightCm: { type: [number, 'null'], minimum: 50, maximum: 300 }
locale:
type: [string, 'null']
enum: [de, en, null]
timezone: { type: string, minLength: 1, maxLength: 64 }
DeviceRegisterInput:
type: object
properties:
token: { type: string, minLength: 8, maxLength: 512 }
bundleId: { type: string, minLength: 1, maxLength: 128 }
locale: { type: string }
appVersion: { type: string }
model: { type: string }
required: [token, bundleId]
IntakeUpdateInput:
type: object
properties:
intakeId: { type: string }
status:
type: string
enum: [taken, skipped, snoozed]
takenAt: { type: string, format: date-time }
snoozedUntil: { type: string, format: date-time }
required: [intakeId, status]
InsightRecommendation:
type: object
properties:
id: { type: string }
label: { type: string }
actionURL: { type: [string, 'null'] }
required: [id, label]
InsightCard:
type: object
properties:
id: { type: string }
title: { type: string }
summary: { type: string }
body: { type: [string, 'null'] }
severity: { $ref: '#/components/schemas/InsightSeverityIos' }
recommendations:
type: array
items: { $ref: '#/components/schemas/InsightRecommendation' }
generatedAt: { type: string, format: date-time }
provider: { type: string }
required: [id, title, summary, severity, recommendations, generatedAt, provider]
InsightCorrelation:
type: object
properties:
id: { type: string }
leftMetric: { type: string }
rightMetric: { type: string }
coefficient: { type: number }
sampleSize: { type: integer }
required: [id, leftMetric, rightMetric, coefficient, sampleSize]
# ─── Admin DTOs ────────────────────────────────────────────
AdminUpdateUserInput:
type: object
properties:
username: { type: string, minLength: 3, maxLength: 30 }
email: { type: [string, 'null'], format: email }
role: { $ref: '#/components/schemas/UserRole' }