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_' } 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_' 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 `. 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' }